TP6.0不支持真正嵌套事务,Db::startTrans()多次调用仅递增计数器,底层仍为单事务;内层rollback会全局清空所有SQL,外层commit因“no active transaction”被忽略,必须用SAVEPOINT实现局部回滚或业务层降级处理。

PHP 本身不支持真正的嵌套事务,MySQL(InnoDB)也不原生支持——所谓“嵌套”,实际是靠 SAVEPOINT 模拟的局部回滚机制。ThinkPHP 6.0 的 Db::startTrans() 多次调用只是递增计数器,底层仍为单事务;误当嵌套用,轻则局部失败连累全局,重则数据全丢还无法恢复。
为什么 Db::startTrans() 看起来像嵌套,实则无效
TP6 在已有活跃事务时再次调用 Db::startTrans(),不会触发新的 PDO::beginTransaction(),只做 transTimes++ 计数。这意味着:
- 三次
Db::startTrans()后调Db::rollback()→ 立即执行PDO::rollback(),清空全部 SQL,包括最外层操作 - 中间某层 rollback 后,
transTimes被重置为 0,后续Db::commit()因 “no active transaction” 被静默忽略 - 内层
Db::transaction()闭包抛异常,不会传导到外层,外层仍可能commit()部分成功语句
想实现“扣款成功、发券失败只回滚发券”,必须用 SAVEPOINT
MySQL 8.0+ 或 PostgreSQL 可用原生 SAVEPOINT 实现真正意义上的局部回滚。TP6 不封装该能力,需手动执行:
Db::startTrans();Db::table('orders')->where('id', $id)->update(['status' => 'paid']);Db::execute('SAVEPOINT issue_coupon');try { Db::table('coupons')->insert(['order_id' => $id, 'code' => $code]);} catch (\Exception $e) { Db::execute('ROLLBACK TO SAVEPOINT issue_coupon');}Db::commit();
注意:SAVEPOINT 名称必须合法(仅字母、数字、下划线),且不可重复;重复定义会覆盖前一个,导致回滚点失效。
立即学习“PHP免费学习笔记(深入)”;
MySQL 5.7 及以下怎么办:业务层降级处理
低版本 MySQL 不支持 SAVEPOINT,唯一安全路径是放弃“自动回滚局部操作”的幻想,改由代码显式控制分支:
- 所有操作仍在同一事务中,但发券失败时不调
rollback(),而是跳过后续依赖逻辑(如通知、日志) - 必须在每个关键步骤后检查返回值,例如
$couponResult = Db::table('coupons')->insert(...) - 失败后不能继续执行“发券成功才做的动作”,否则会产生副作用(比如重复发消息、错记流水)
这个方案不依赖数据库特性,但要求整个业务链路对失败有明确响应策略——漏掉一处判断,就可能引发数据不一致。
SAVEPOINT 不是银弹:它只解决“事务内回退到某点”,不能跨连接、不能跨库、不能替代业务校验。最容易被忽略的是——TP6 的 Db::execute() 若混用不同连接实例(比如读写分离开启时没指定写连接),SAVEPOINT 会落在错误连接上,导致 ROLLBACK TO 找不到目标点,静默失败。



















