事务未生效需检查:是否在DB::transaction()闭包内执行所有操作;MySQL引擎是否为InnoDB;避免嵌套事务误用;确保同一连接复用;移出非DB操作;控制事务粒度防锁表。

事务没生效?检查是否在 DB facade 或模型方法里调用了 DB::transaction()
Laravel 的事务默认只对 DB::transaction() 包裹的代码块生效,模型的 save()、update() 等操作不会自动加入外层事务——除非你显式用 DB::beginTransaction() + DB::commit()/DB::rollback() 手动控制。
常见错误现象:DB::transaction() 里调用了 Eloquent 模型方法,但某步出错后数据仍部分写入。
- 确认所有数据库操作都在
DB::transaction()的闭包内,不要把模型操作写在闭包外 - 避免在事务中调用其他未受控的服务方法,它们可能内部又开了新连接或用了不同 DB 实例
- MySQL 引擎必须是 InnoDB;MyISAM 不支持事务,哪怕写了
DB::transaction()也完全无效
嵌套事务怎么处理?Laravel 默认不支持真嵌套,得手动管理
Laravel 的 DB::transaction() 是“扁平化”的:第二次调用不会开启新事务,而是复用当前事务上下文。一旦外层 rollback,整个都回滚,没有保存点(savepoint)语义。
使用场景:比如订单创建时要同时写订单主表、明细表、库存扣减,其中库存服务又封装了自己的一组操作,你希望它失败时不连带干掉整个订单流程。
- 如需局部回滚,改用
DB::transaction(function () { ... }, $attempts)的重试机制,而非嵌套调用 - 真需要 savepoint,得绕过 Laravel 封装,直接执行
DB::statement('SAVEPOINT sp1')和DB::statement('ROLLBACK TO SAVEPOINT sp1') - 注意 PDO 默认关闭
PDO::ATTR_EMULATE_PREPARES,某些 savepoint 语句在预处理模式下会报错
事务里查不到刚插入的数据?注意读已提交隔离级别和连接复用
MySQL 默认隔离级别是 REPEATABLE READ,但在 Laravel 中,如果你在事务里用 Model::create() 插入一条记录,紧接着用 Model::find() 查,大概率能查到——前提是没换连接。
容易踩的坑:DB::transaction() 内部如果触发了队列任务、HTTP 请求或跨进程调用,那些操作走的是新连接,看不到未提交的数据。
- 事务内所有查询必须走同一个 PDO 连接,Laravel 默认会复用,但加了
DB::connection('xxx')->...显式指定连接名时要确保一致 - 不要在事务里 dispatch 队列任务并期望它读到刚插入的 ID;改用
dispatchNow()或把 ID 作为参数传进去 - 若用
DB::select()原生查询,记得用DB::raw()处理变量,别拼字符串,否则可能被隔离级别“保护”住
长时间运行的事务导致锁表?控制粒度比加 try/catch 更关键
事务不是越长越好。一个耗时 5 秒的 DB::transaction() 会持锁至少 5 秒,尤其涉及 SELECT ... FOR UPDATE 时,极易引发阻塞甚至死锁。
性能影响:锁等待时间超过 innodb_lock_wait_timeout(默认 50 秒),会抛出 SQLSTATE[HY000]: General error: 1205 Deadlock found when trying to get lock。
- 把非数据库操作(如发邮件、调第三方 API、文件写入)全部移出事务闭包
- 批量插入改用
DB::table()->insert()而非循环Model::create(),减少语句数量和锁持有时间 - 高并发场景下,优先考虑应用层幂等+补偿,而不是靠长事务保一致性
事务的边界从来不在语法上,而在业务语义里——哪一步失败了可以安全重试,哪一步失败了必须整体放弃,这个判断比怎么写 DB::transaction() 重要得多。











