db::transaction()闭包方式最常用且安全,因它自动托管事务生命周期:无异常则提交,抛exception/throwable则回滚,并清理连接;支持超时、死锁重试及eloquent操作,但不可混入http请求、队列分发等不可回滚操作。

直接用 DB::transaction() 就行,90% 的场景闭包写法足够安全、简洁,不用手动配对 beginTransaction()/commit()/rollback()。
DB::transaction() 闭包写法为什么最常用
它把事务生命周期完全托管给 Laravel:闭包里没异常就自动 commit,抛出任何 Exception 或 Throwable 就立刻 rollback,连连接清理都做了。你只管写业务逻辑,不用操心“万一忘了 rollback 怎么办”。
- 所有 Eloquent 操作(
save()、create()、update()、关联同步等)都在同一事务上下文中 - 支持超时控制,比如
DB::transaction(fn () => {}, 15)表示最多等 15 秒获取行锁 - 内部已启用死锁重试(默认 1 次),遇到
Deadlock found when trying to get lock会自动重试闭包 - 别在闭包里调
dispatch()、Notification::send()、Cache::put()——这些操作不受事务保护,发出去就收不回
事务不回滚?先查数据库引擎是不是 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提前报错,不是事务失效,是锁竞争
嵌套事务不能直接套 DB::transaction()
在 DB::transaction() 闭包里再调一次 DB::transaction(),看起来像嵌套,其实不是。外层一失败,内层所有变更全丢,而且不会报错——容易误以为“子事务成功了”,实际根本没独立边界。
- 真需要子事务效果(比如某步失败只回滚局部),得用保存点:
DB::statement('SAVEPOINT sp1');+DB::statement('ROLLBACK TO SAVEPOINT sp1'); - 或者干脆拆开:外层事务管主流程,子逻辑抽成独立方法,由调用方决定是否包裹新事务
- 不要依赖“嵌套”来隔离错误,而是靠清晰的错误分类和提前校验(比如先
validate()再进事务)
队列任务必须 afterCommit() 才算真正原子
事务里 dispatch(new SendEmailJob($user)) 是危险操作:哪怕后续 rollback,邮件任务已经进队列,照样执行。结果就是“用户没注册成功,但邮件发出去了”。
- 必须改成
dispatch(new SendEmailJob($user))->afterCommit(); - 或者让任务类实现
ShouldQueueAfterCommit接口,一劳永逸 -
afterCommit()只对database和redis驱动生效,sync驱动不走队列,自然无所谓 - 别在事务里
dispatchSync(),它会立即执行,同样绕过事务控制
事务真正的复杂点不在语法,而在边界——哪些操作能被回滚(只有当前连接上的 SQL),哪些不能(HTTP 请求、文件写入、缓存修改、队列分发)。一旦混进去一个不可回滚的操作,整个事务的原子性就破了。











