MySQL事务不生效的首要原因是存储引擎不支持事务,如MyISAM引擎不支持ACID特性,即使使用DB::transaction()也无法回滚;应通过SHOW TABLE STATUS确认Engine为InnoDB,并用ALTER TABLE或迁移文件指定ENGINE=InnoDB。

事务没生效?检查是否用了不支持事务的存储引擎
MySQL 默认的 MyISAM 引擎不支持事务,哪怕写了 DB::transaction() 也完全无效,执行失败也不会回滚。Laravel 的事务底层依赖数据库引擎能力,不是 PHP 层面模拟的。
确认方式:运行 SHOW TABLE STATUS LIKE 'users';,看 Engine 列是不是 InnoDB。如果不是,用 ALTER TABLE users ENGINE=InnoDB; 转换。
- 迁移文件里加
$table->engine = 'InnoDB';是最稳妥的预防手段 - SQLite 和 PostgreSQL 默认支持事务,但 SQLite 在多进程写入时可能因锁机制提前抛出
SQLITE_BUSY - 如果用的是 MySQL Group Replication 或某些云数据库只读节点,事务行为可能受限,需查具体文档
DB::transaction() 的嵌套陷阱与正确写法
看似能嵌套,实际 Laravel 默认不支持真嵌套事务(savepoint 机制需手动开启)。直接在事务内再调 DB::transaction(),外层一失败,内层所有操作全丢,且不会报错——容易误以为“内层成功了”。
正确做法是用 DB::transaction() 包裹全部相关操作,或手动用 DB::beginTransaction() + DB::commit()/DB::rollback() 配合 try/catch:
DB::beginTransaction();
try {
User::create([...]);
Order::create([...]);
DB::commit();
} catch (\Exception $e) {
DB::rollback();
throw $e;
}
-
DB::transaction()内部已含异常捕获和自动回滚,比手写更简洁,推荐优先使用 - 传入第二个参数可设超时,如
DB::transaction(fn () => {...}, 10)表示最多等 10 秒获取锁 - 不要在事务中调用外部 HTTP 请求、队列推送或文件写入——这些操作无法回滚,会破坏数据一致性
模型事件、Observer 和事务的冲突点
Model 的 creating、saving 等事件,以及 Observer 里的逻辑,会在事务提交前触发。但如果这些回调里又开了新连接(比如用 DB::connection('other')),就脱离当前事务上下文,无法被回滚。
常见翻车场景:
- 在
User::created事件里调Notification::route(...)->notify(...),邮件发出去了,但事务回滚后用户实际没创建成功 - Observer 中调用
Cache::forget(),缓存删了,但数据库没改,导致缓存与 DB 不一致 - 使用
DB::unprepared()执行原生 SQL,若未显式指定连接,可能走默认连接而非事务连接
解决思路:把副作用操作(通知、缓存、日志)移到事务外,或用 DB::afterCommit() 延迟到提交成功后再执行。
队列任务里用事务必须注意连接复用问题
Laravel 队列 worker 启动后会长期复用数据库连接。如果一个任务里开启事务但没正常结束(比如抛出未捕获异常、超时退出),该连接会卡在 in transaction 状态,后续任务可能复用这个“脏连接”,导致 SQLSTATE[HY000]: General error: 2006 MySQL server has gone away 或诡异的锁等待。
关键措施:
- 队列任务中务必用
DB::transaction()或完整try/catch,确保无论成功失败都明确commit或rollback - 避免在队列里长时间持有事务(如循环中逐条处理并更新),改用批量操作或分块提交
- 配置
queue.worker.maxTries和queue.worker.timeout防止僵尸事务堆积
真正棘手的从来不是怎么写事务语法,而是哪些操作会悄悄逃出事务边界——连接切换、异步调用、第三方 SDK 的隐式查询,都得一个个对齐验证。


















