thinkphp事务必须用db::transaction()闭包写法,无参调用不启动事务;tp5.1+手动starttrans因连接惰性初始化极易失效;需确保innodb引擎、关闭autocommit、异常穿透且不混用连接。

ThinkPHP 的数据库事务不是“写了 Db::transaction() 就自动兜底”的黑盒,它高度依赖连接一致性、异常穿透和引擎支持——用错一步,数据就可能部分写入、无法回滚。
Db::transaction() 闭包必须传函数,否则事务根本不启动
很多人写成 Db::transaction() 单独一行,以为开启了事务,结果所有 SQL 都直写数据库。这是因为无参调用只返回一个事务对象,不执行任何操作。
- 正确写法:
Db::transaction(function () { Db::table('order')->insert([...]); Db::table('stock')->dec('num', 1); }); - 闭包内任意位置抛出未捕获的
\Exception(如throw new \Exception('库存不足')),框架自动回滚 - 闭包正常结束,自动
commit();无需手写try-catch,也别在闭包里try-catch吞异常,否则等于告诉框架“我处理好了”,它就会提交 - 想提前退出并回滚?直接
return false;(TP 6.0.10+ 支持)
手动事务 Db::startTrans() 在 TP5.1+ 基本不可靠
TP5.1 起默认启用连接惰性初始化,Db::startTrans() 可能还没拿到 PDO 实例,后续 Db::table() 或模型操作却新建了连接——事务指令发给 A 连接,SQL 执行在 B 连接,看起来像“回滚失败”,其实是根本没进同一个事务上下文。
- 混用模型和 Db 查询(如
User::create()+Db::table('log')->insert())极易触发此问题 - 开启读写分离(
'deploy' => 1)或自定义连接配置时,风险更高 - 若真需手动控制(比如分支逻辑复杂、要条件提交),务必统一使用
Db::connect('default')显式获取连接,并全程复用该实例
事务不回滚,90% 是数据库层或配置问题
代码看着没问题,但数据还是被改了——问题往往不在 PHP 层。
- 检查表引擎:
SHOW CREATE TABLE stock,确认输出含ENGINE=InnoDB;MyISAM 表在事务中执行的语句会立刻生效,rollback()对它完全无效 - 确认自动提交关闭:
Db::connect()->getPdo()->getAttribute(PDO::ATTR_AUTOCOMMIT)必须为0,不是1 - 避免混用 InnoDB 和 MyISAM 表:哪怕只有一条 INSERT 到 MyISAM 表,这条语句就脱离事务控制
- 事务中禁止 DDL 操作(如
ALTER TABLE)、禁止SELECT ... FOR UPDATE外的非事务性查询(某些驱动下会隐式提交)
嵌套事务、跨模型、高并发场景的硬限制
ThinkPHP 不支持真正的嵌套事务(savepoint),也不支持跨库 XA 事务——这些不是“没实现”,而是设计上明确不支持,强行用会导致数据不一致。
-
Db::transaction()嵌套调用:内层只是普通闭包,不会建保存点;外层异常才回滚,内层抛异常只中断自己 - 跨模型操作(如
UserModel::create()+OrderModel::create()):模型默认各自建连接,startTrans()只作用于当前 Db 门面绑定的那个连接 - 高并发下单卡死?大概率是事务范围过大:把 HTTP 请求、文件读写、大表查询塞进事务,锁持有时间过长,引发
Waiting for table metadata lock - 真正安全的做法:事务只包库存扣减、订单生成、日志插入这三步;通知、统计、ES 写入等全部移出事务
最常被忽略的一点:事务不是隔离的“保险箱”,它是连接+引擎+异常传播共同作用的结果。任何一个环节断开——连接没复用、异常被吞、引擎不支持、DDL 插入——都会让事务失效,且往往静默发生,查日志都看不到报错。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











