db::transaction()闭包写法90%场景够用,但必须确保所有db操作在闭包内、mysql引擎为innodb、避免非db操作和连接切换,嵌套调用不产生子事务,局部回滚需手动savepoint。

直接用 DB::transaction() 闭包写法,90% 场景够用、安全、不用手动配对 beginTransaction()/commit()/rollback()。
DB::transaction() 闭包里必须包含所有 DB 操作
常见错误是把 Eloquent 模型操作写在闭包外,比如:
DB::transaction(function () {
User::create([...]);
});
Order::create([...]); // ❌ 这行不在事务内,失败也不会回滚
正确做法是确保所有写操作(save()、update()、delete()、DB::insert() 等)都在闭包内执行。
- 模型的
creating、saving等事件会触发,但事件回调里不能新开连接(如DB::connection('other')),否则脱离事务上下文 - 不要在闭包里调
dispatch()、Notification::send()、Cache::put()—— 这些操作发出去就收不回 - 若需队列任务,必须加
->afterCommit(),否则事务回滚后任务仍会执行
事务没生效?先查 MySQL 引擎是不是 InnoDB
写了 DB::transaction() 却发现数据没回滚,八成是表用了 MyISAM。MySQL 的 MyISAM 引擎根本不支持事务,Laravel 层再怎么包也没用。
- 运行
SHOW TABLE STATUS LIKE 'users';,看Engine列是否为InnoDB - 不是的话,执行
ALTER TABLE users ENGINE=InnoDB;转换(大表建议低峰期操作) - 更稳妥的是在迁移文件里写
$table->engine = 'InnoDB';,从源头杜绝 - SQLite 和 PostgreSQL 默认支持事务,但 SQLite 多进程并发写时可能因
SQLITE_BUSY报错,不是事务失效,是锁竞争
别信“嵌套事务”,真要局部回滚得用 SAVEPOINT
在 DB::transaction() 闭包里再调一次 DB::transaction(),看起来像嵌套,其实不是:外层一失败,内层所有变更全丢,且不报错 —— 容易误以为“子事务成功了”。
- Laravel 默认不提供保存点(savepoint)语义,
DB::transaction()是扁平化的 - 需要局部回滚(比如订单创建中库存扣减失败,只回滚那一步),得手动执行:
DB::statement('SAVEPOINT sp1');和DB::statement('ROLLBACK TO SAVEPOINT sp1'); - 注意 PDO 默认关闭
PDO::ATTR_EMULATE_PREPARES,某些 savepoint 语句在预处理模式下会报错 - 更推荐的做法是拆逻辑:把可能失败的子流程抽成独立方法,由调用方决定是否包裹新事务
事务里查不到刚插入的数据?检查连接复用和隔离级别
在事务里用 User::create() 插入记录,紧接着 User::find() 查不到,大概率是换了连接或读取到了旧快照。
- Laravel 默认复用同一 PDO 连接,但如果你显式写了
DB::connection('xxx'),要确保全程用同一个连接名 - MySQL 默认隔离级别是
REPEATABLE READ,事务内能查到自己刚写的,但跨连接(比如队列任务、HTTP 请求)就看不到未提交数据 - 别在事务里用
dispatch()后再查刚生成的 ID;要么改用dispatchNow(),要么把 ID 当参数传进去 - 用
DB::select()原生查询时,务必用DB::raw()处理变量,别拼字符串,否则可能被隔离级别“保护”住
最常被忽略的其实是引擎类型和连接复用 —— 写了事务却没生效,往往不是代码问题,而是表没设成 InnoDB,或者某处悄悄开了新连接。











