thinkphp 6.0 不支持真正的嵌套事务,其 db::transaction() 在已有事务时仅顺序执行闭包而不开启新事务,内层异常不传导至外层,导致数据不一致;正确做法是将所有操作置于同一事务闭包中,或通过 savepoint 实现局部回滚。

ThinkPHP 6.0 不支持真正的嵌套事务,所谓“嵌套调用 Db::transaction()”实际只维持单层事务状态。内层事务的 commit 或 rollback 操作会被忽略,异常也不会自动传导至外层,导致外层仍可能提交部分成功操作——这是复杂业务中最隐蔽的数据一致性风险。
为什么 TP6 的嵌套事务会失效
底层依赖 PDO 的 beginTransaction(),而 PDO 和 MySQL 均不原生支持嵌套事务。TP6 在检测到已存在活跃事务时,Db::transaction() 不会真正开启新事务,仅执行闭包逻辑,且不接管回滚控制。
- 外层事务开启后,内层再次调用
Db::transaction()→ 无新事务,仅顺序执行 SQL - 内层抛异常 → 仅中断当前闭包,外层事务 unaware,继续走到
commit() - 结果:用户表更新了,订单没创建,库存却扣减了
正确做法:统一收口 + 显式判断
所有关联操作必须放在同一个事务闭包中,避免逻辑分散。若业务模块需复用,应提取为无事务的纯数据操作函数,由顶层统一包裹。
- ✅ 正确写法:
Db::transaction(function () { userCreate(); orderCreate(); stockDeduct(); }); - ❌ 错误写法:
Db::transaction(fn() => userCreate()); Db::transaction(fn() => orderCreate()); - 若必须分层,改用
Db::startTrans()+ 手动状态标记(如$shouldRollback = true),在 catch 中设标志,外层根据标志决定是否Db::rollback()
需要局部回滚?用 SAVEPOINT 模拟
TP6 虽不封装 savepoint API,但可直连 PDO 执行原生语句实现精细控制。适用于“主流程必须成功,日志或通知失败可容忍”的场景。
- 开启事务后,执行
Db::execute('SAVEPOINT sp_log'); - 尝试记录日志,失败则
Db::execute('ROLLBACK TO SAVEPOINT sp_log'); - 主流程无异常 →
Db::commit();有异常 →Db::rollback() - 注意:MySQL 8.0+ 支持 savepoint,低版本需确认兼容性
上线前必查的三个信号
这些迹象说明事务设计已埋雷:
- 代码里出现多个
Db::transaction()调用,且分布在不同 Service 方法中 - 事务闭包内有 try/catch 但未 re-throw 异常,或吞掉异常后继续执行
- 数据库监控发现 “部分写入” 记录(如订单表有数据,但关联支付流水为空)











