laravel 事务生效需满足四前提:mysql 必须用 innodb 引擎;所有 db 操作须在闭包内且共用同一连接;非 db 操作(如队列、文件、邮件)不可放入事务;嵌套调用不创建新事务,需手动 savepoint 实现局部回滚。

事务在 Laravel 中不是“写了就生效”的魔法开关,它高度依赖底层数据库能力、连接复用机制和代码组织方式。不按规则写,DB::transaction() 就是摆设。
MySQL 引擎必须是 InnoDB 才能回滚
MyISAM 表无论套多少层 DB::transaction(),失败后数据照旧写入——它根本不支持事务。这是最常被忽略的硬性前提。
- 执行
SHOW TABLE STATUS LIKE 'orders';,确认Engine列显示InnoDB - 迁移文件里显式指定:
$table->engine = 'InnoDB';,比事后ALTER TABLE更可靠 - SQLite 默认支持,但多进程写入时可能因
SQLITE_BUSY报错;PostgreSQL 无此问题
所有操作必须在闭包内且走同一连接
DB::transaction() 只包裹闭包里的代码,模型方法如 User::create() 不会自动“感知”外层事务——除非它们也在闭包里调用。
- 错误写法:
DB::transaction(fn () => User::create([...]))✅ 正确;但$user = User::create([...]); DB::transaction(fn () => ...)❌ 失效 - 避免在事务中调用
DB::connection('log')->insert(),它开了新连接,不受当前事务控制 - 队列任务、HTTP 请求、文件写入等非 DB 操作不能放闭包里——它们无法回滚,反而会制造“已发邮件但订单没创建”的脏状态
嵌套事务不等于 savepoint,真需要局部回滚得手动写
Laravel 的 DB::transaction() 是扁平的:第二次调用不会建新事务,而是复用当前上下文。外层一 rollback,里层全丢,且不报错。
- 不要写:
DB::transaction(fn () => { DB::transaction(fn () => {...}); });—— 这不是嵌套,只是重复进入同一事务 - 要实现“扣库存失败不影响用户创建”,得手动加保存点:
DB::statement('SAVEPOINT sp1')和DB::statement('ROLLBACK TO SAVEPOINT sp1') - 注意 PDO 预处理模式下部分
SAVEPOINT语句会失败,可临时设PDO::ATTR_EMULATE_PREPARES => true
事务里查不到刚插入的数据?先看隔离级别和连接是否一致
在事务中用 Model::create() 插入后立刻 Model::find() 查不到,大概率是因为查走了另一个连接,或数据库隔离级别限制。
- 默认
REPEATABLE READ下,只要没换连接(即没显式调DB::connection('xxx')),查得到刚插的记录 - 如果用了
DB::unprepared()原生 SQL,务必传入连接实例:DB::connection()->unprepared(...),否则可能走默认连接 - 事务内禁止
dispatch()队列任务并指望它读到未提交数据;改用dispatchNow()或把 ID 当参数传进去
事务真正的复杂点不在语法,而在于边界——它只管当前 PDO 连接上的 SQL 执行,对模型事件、缓存操作、外部服务调用完全无感。一旦跨出这个边界,就得靠补偿、重试或 Saga 来兜底。











