mysql不支持嵌套事务,start transaction会隐式提交当前事务;所谓“嵌套”实为框架用savepoint模拟,tp8默认禁用,laravel/doctrine通过计数器+保存点实现,但内层异常不触发外层回滚。

PHP 事务嵌套“无效”不是 Bug,而是 MySQL 本身不支持嵌套事务——START TRANSACTION 在已有事务中执行,会触发隐式提交,PDO 层无法绕过。所谓“嵌套成功”,只是框架用 SAVEPOINT 模拟的局部回滚点,外层仍会 COMMIT,数据可能部分落库。
MySQL 原生不支持嵌套事务,START TRANSACTION 会隐式提交
这是 MySQL 官方明确说明的行为:Transactions cannot be nested. This is a consequence of the implicit commit performed for any current transaction when you issue a START TRANSACTION statement。也就是说,第二次调用 START TRANSACTION(或 PDO 的 beginTransaction())时,MySQL 会先自动提交当前事务,再开启新事务。PDO 层无法拦截或阻止这个行为,所以“嵌套”在数据库层面根本不存在。
常见错误现象:
- 外层事务中调用另一个
Db::transaction(),结果外层没报错但数据部分写入 -
ROLLBACK后发现日志表有记录、业务表却没更新——因为日志写在 SAVEPOINT 之前 - CLI 命令里测试嵌套事务,始终不生效——命令未初始化完整事务上下文,
Db::getPdo()可能返回新连接
ThinkPHP 8 默认禁用 SAVEPOINT,嵌套直接抛 RuntimeException
TP8 的 Db::transaction() 默认不启用保存点机制,两次调用会立刻报错:RuntimeException: Transaction already started。这不是配置遗漏,是框架主动拒绝模拟——避免开发者误以为“嵌套=原子隔离”。
若手动传 true 启用 SAVEPOINT(即 Db::transaction(fn() => ..., true)),必须同时满足三个硬性条件:
- 数据库引擎为
InnoDB(SHOW CREATE TABLE your_table必须含ENGINE=InnoDB) - 事务隔离级别不能是
READ UNCOMMITTED(SELECT @@tx_isolation应返回REPEATABLE-READ或READ-COMMITTED) - 连接未被模型自动切换(混用
UserModel::create()和Db::table('order')极易跨连接,事务失效)
任意一项不满足,SAVEPOINT 创建失败,后续 ROLLBACK TO SAVEPOINT 就会报:SQLSTATE[HY000]: General error: 1305 SAVEPOINT TP_SAVEPOINT_1 does not exist。
Laravel/Doctrine 的“嵌套”本质是事务计数 + SAVEPOINT
Laravel 和 Doctrine 并不创造新事务,而是维护一个 $transTimes 计数器:
-
beginTransaction():计数 +1;仅当$transTimes === 1时才真调$pdo->beginTransaction(),否则创建SAVEPOINT -
commit():计数 -1;仅当减到 0 时才真调$pdo->commit() -
rollback():若计数 >1 且支持 SAVEPOINT,则执行ROLLBACK TO SAVEPOINT;否则真回滚并清零
这种设计规避了 MySQL 隐式提交问题,但代价是:内层异常不会导致外层回滚。例如订单创建事务中调用库存扣减事务,库存失败只回滚到保存点,订单仍会提交——这和直觉中的“嵌套事务”完全不同。
真需要局部回滚?绕过 Db::transaction(),手写 SAVEPOINT
如果业务逻辑确实要求“先记日志、再试更新,失败只撤回更新”,比框架封装更可控的方式是直接操作 PDO:
$pdo = Db::getPdo();
$pdo->exec('SAVEPOINT log_before_update');
try {
$pdo->exec('UPDATE inventory SET stock = stock - 1 WHERE id = 123');
} catch (\Exception $e) {
$pdo->exec('ROLLBACK TO SAVEPOINT log_before_update');
throw $e;
}
这样你能精确控制保存点命名、回滚范围,也避开框架对连接复用、模型切换、隔离级别的隐式约束。但要注意:所有语句必须走同一个 $pdo 实例,且不能混用 Query Builder 和原生 exec()(后者不参与事务上下文管理)。
最常被忽略的一点:事务边界必须与业务语义对齐,而不是函数调用堆栈。一个“嵌套事务”的需求,往往暴露的是职责拆分不合理——比如把日志记录和核心更新塞进同一个函数,又各自加事务。与其花精力模拟嵌套,不如重构为单事务内分步 + 显式错误判断。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











