db::transaction()仅对未被捕获的异常自动回滚;若闭包内try-catch吞掉exception且未re-throw,框架默认commit;php 8.0+需捕获throwable,跨连接、myisam引擎、嵌套事务均导致回滚失效。

Db::transaction() 里 catch 了 Exception,事务就自动 commit 了
ThinkPHP 的 Db::transaction() 只对「未被捕获的异常」做回滚。一旦你在闭包里写了 try { ... } catch (\Exception $e) { ... } 却没 throw $e,框架根本收不到错误信号,会默认执行 commit()。
常见错误写法:
Db::transaction(function () {
try {
Db::name('order')->insert(['user_id' => 1]);
throw new \Exception('模拟失败');
} catch (\Exception $e) {
Log::error($e->getMessage());
// ❌ 没有 re-throw,事务照常提交
}
});
- PHP 8.0+ 还要注意:
\Error(比如Fatal error)不会被\Exception捕获,得用\Throwable - 想记录日志又保证回滚?catch 末尾必须加
throw $e; - 更干净的做法是直接去掉 try-catch,让异常穿透出去
用了 db('table') 或 Db::connect(),事务就跨连接失效了
Db::transaction() 闭包内部所有操作必须复用同一个 PDO 实例,否则事务控制和 SQL 执行不在一条连接上——rollback() 执行了,但数据还在。
典型表现:
-
rollback()没报错,但查数据库发现数据已写入 - 日志里看不到
BEGIN,或者报There is no active transaction - 用了
db('user')助手函数,或Db::connect('slave')显式切换连接
正确做法:
- 闭包内只用
Db::name('table')或模型方法(如User::create()) - 禁用
db('table')、Db::connect()、Db::master()等显式连接操作 - 读写分离场景下查刚插入的数据,用
Db::master()->name('user')->select()强制走主库
MySQL 表引擎不是 InnoDB,事务根本不起作用
MyISAM 引擎不支持事务,无论你怎么写 Db::transaction() 或 Db::startTrans(),都不会回滚。这是最隐蔽也最容易被忽略的底层限制。
检查方式:
- 执行
SHOW CREATE TABLE think_user;,看 ENGINE=InnoDB - 如果显示
ENGINE=MyISAM,立刻改:ALTER TABLE think_user ENGINE=InnoDB ROW_FORMAT=DYNAMIC; - 注意:有些低版本 MySQL 默认建表用 MyISAM,且
ROW_FORMAT必须设为DYNAMIC才能兼容 TP6 的事务行为
嵌套 Db::transaction() 或混用 startTrans(),状态直接错乱
PDO 和 MySQL 都不支持真嵌套事务。Db::transaction() 嵌套调用时,内层事务会被静默忽略;外层仍会 commit,导致部分数据写入。
同样危险的是混用模式:
- 在
Db::transaction()闭包里又调用Db::startTrans() - 闭包外手动调用
Db::commit()或Db::rollback(),会报错或静默失败 - 事务回滚后连接可能卡在异常状态,后续查询直接抛
PDOException: Cannot execute queries while other unbuffered queries are active
稳妥做法只有一个:所有 DB 操作收进一个顶层 Db::transaction() 闭包,别拆、别分层、别跨库。复杂逻辑用函数封装,但别让它自己开事务。











