ThinkPHP 5.1+ 中必须使用 Db::transaction() 闭包写法,因 startTrans/commit/rollback 手动事务在连接惰性初始化机制下无法绑定同一 PDO 实例,易导致事务指令与 SQL 执行分离于不同连接,造成看似“未回滚”实则事务上下文错位。

ThinkPHP 事务必须用 Db::transaction() 闭包写法,手动调用 startTrans/commit/rollback 在 5.1+ 版本中极易失效,不是“不推荐”,而是“大概率不工作”。
为什么 startTrans 在 TP5.1+ 里经常不生效
根本原因不是写法错,而是连接没绑定住:TP 默认启用「连接惰性初始化」,startTrans() 调用时可能还没真正拿到 PDO 实例;后续 Db::table() 或模型操作却触发了新连接创建,导致事务指令发给了 A 连接,SQL 却执行在 B 连接上——看起来像“没回滚”,其实是压根不在同一个事务上下文里。
尤其当你混用模型(User::create())和原生查询(Db::table('log')->insert()),或启用了读写分离('deploy' => 1)、连接池配置时,这个问题会立刻暴露。
- 错误现象:
rollback()调用了但数据仍被写入;commit()成功却报PDOException: MySQL server has gone away - 不会报错,但事务级别降级(比如传了
'READ COMMITTED'字符串,PDO 直接忽略,用默认隔离级别) - 事务执行超时(如卡在外部 HTTP 请求)后,MySQL 主动断连,
commit()抛异常,但rollback()已无意义
Db::transaction() 是唯一可靠写法
它强制复用同一连接、自动捕获未处理异常、异常时必定回滚、成功时必定提交——不用手写 try/catch,也不用担心漏掉 rollback。
立即学习“PHP免费学习笔记(深入)”;
正确示例:
use think\facade\Db;
Db::transaction(function () {
User::create(['name' => 'Alice']);
Db::table('log')->insert(['msg' => 'user created']);
// 中间任何一步抛异常,整个闭包退出,事务自动回滚
});
- 支持嵌套:内部再调
Db::transaction()会自动转为 savepoint,不是新开事务 - 参数可选事务隔离级别:
Db::transaction(fn() => ..., 'SERIALIZABLE'),但注意字符串值必须是 PDO 常量对应字面量('SERIALIZABLE'可,'serializable'不行) - 不能用于跨库 XA 事务,那是
Db::transactionXa()的职责,且需数据库和服务端双重支持
模型层事务的陷阱:别直接调 $model->startTrans()
模型实例持有的连接不一定和 Db 门面一致,尤其当模型指定了独立连接配置(如 protected $connection = 'slave'),而你在事务里又混用了 Db::table(),就等于把两个不同连接的操作强行塞进一个事务——不可能原子。
如果你非要用模型,确保:
- 所有操作都走同一个模型实例(
$user = new User(); $user->startTrans(); ... $user->commit()) - 或统一走
Db::transaction(),里面只调模型静态方法(User::create()、Order::update()),它们底层仍会通过Db门面调度,受闭包连接控制 - 避免在事务闭包里调
Db::connect('other_db')——那会脱离当前事务上下文
事务之外必须检查的三件事
再正确的事务写法,遇上这三点也会白搭:
- 表引擎必须是
InnoDB(MyISAM完全不支持事务,SHOW CREATE TABLE user看ENGINE=InnoDB) - MySQL 配置项
autocommit=1必须保持开启(TP 依赖它来判断是否处于显式事务中) - 事务内避免耗时操作:如远程 API 调用、大文件读写、循环插入万条数据——wait_timeout 是连接级的,不是事务级的,卡太久连接断了,
commit就必然失败
最常被忽略的是:事务只保证数据库写操作的原子性,不解决并发更新冲突。需要 SELECT ... FOR UPDATE、乐观锁字段或幂等 token 配合使用,否则高并发下仍可能脏写。



















