DB::transaction 闭包内抛异常会自动回滚,但循环中 catch 吞掉异常则事务提交;正确做法是不捕获或透出异常,需跳过失败项应先批量校验,跨 PDO 操作不受事务保护。

DB::transaction 闭包内抛异常会自动回滚,但循环中 catch 吞掉异常就失效
很多人在 DB::transaction 里写 foreach 循环处理一批数据,又在里面加了 try/catch 捕获单条失败,结果发现整批数据没回滚——问题就出在异常被吞了。Laravel 的自动回滚只响应「未被捕获的异常」,一旦你在循环里 catch 住并 continue 或 return,事务就认为“一切正常”,最终调用 commit。
正确做法是:循环中不自己 catch,让异常透出;或改用手动事务 + 显式 rollback。
- 不要在
DB::transaction闭包里对每条记录做try/catch - 若需跳过单条失败项,应先收集所有待操作数据,再批量校验(如用
throw_if) - 必须捕获时,记得在
catch块末尾throw $e,否则回滚不会触发
批量插入失败时,用 throw_if 替代 if-return 防止软退出
常见错误是在事务闭包开头查库存、余额等状态,然后写 if ($stock 。这看起来像“提前退出”,实则等于静默成功,事务照常提交。Laravel 不会把 <code>return 当成失败信号。
throw_if 是更安全的替代:它在条件为真时抛出异常,确保事务中断并回滚。
- 错:
if (DB::table('stocks')->where('id', $pid)->value('qty') - 对:
throw_if(DB::table('stocks')->where('id', $pid)->value('qty') - 注意
throw_if第二个参数必须是Exception或其子类,不能传字符串
需要局部回滚?用 SAVEPOINT + ROLLBACK TO 而非嵌套 transaction
Laravel 不支持真正嵌套事务,DB::transaction 套 DB::transaction 只是复用外层事务,内层 rollback 不起作用。想实现“某步失败只回滚那一步”,得用数据库原生的保存点机制。
MySQL(InnoDB)、PostgreSQL、SQLite 都支持,SQL Server 也行。关键不是 Laravel 提供封装,而是你手写 SQL 控制。
- 设置保存点:
DB::statement('SAVEPOINT before_import') - 局部回滚:
DB::statement('ROLLBACK TO SAVEPOINT before_import') - 释放保存点(可选):
DB::statement('RELEASE SAVEPOINT before_import') - 别依赖
DB::savepoint()—— Laravel 官方没有这个方法,部分博客示例是伪造 API
模型事件和队列操作不在事务作用域内,需显式处理
Eloquent 模型的 saving、created 等事件,在 DB::transaction 中触发时仍处于事务内,但如果你在事件里调用了 dispatch(new SendEmailJob),这个队列任务**不会**被回滚——它已脱离当前请求生命周期。
同样,模型中用 static::created 触发的邮件发送、日志记录,若没走队列且直接执行 DB 写入,也会被事务包裹;但一旦进队列,就失去原子性保障。
- 事务内避免同步调用外部服务(如 HTTP 请求、邮件发送)
- 必须发邮件?用
afterCommit或延迟到DB::commit()后再 dispatch - 监听模型事件时,检查是否在事务中:
DB::transactionLevel() > 0,决定是否 defer 操作
DB:: 方法,就不算事务一部分。


















