事务未回滚主因是异常被try...catch吞没导致框架无法捕获;嵌套事务不生效因pdo不支持;读不到新数据多因读写分离或隔离级别;回滚后连接状态可能异常,需手动管理。

事务没回滚?检查 Db::transaction() 是否被异常中断
ThinkPHP 的事务回滚依赖于 PHP 异常传播机制。如果在 Db::transaction() 闭包里发生异常但被 try...catch 吞掉,事务就不会回滚——它压根不知道出错了。
- 正确做法:让异常穿透闭包,由框架自动捕获并触发回滚
- 错误写法:
try { Db::transaction(function() { ... }); } catch (\Exception $e) { ... }—— 这样事务已提交,再 catch 没用 - 想自定义错误处理?改用
Db::startTrans()+Db::commit()/Db::rollback()手动控制 - 注意:闭包内调用的模型方法、查询构造器都必须在同一个数据库连接下,跨连接(如不同配置名)事务不生效
嵌套事务不回滚?ThinkPHP 默认不支持真嵌套
Db::transaction() 嵌套调用时,外层事务不会因内层失败而回滚——ThinkPHP 实际只维护一层事务状态,内层的 commit 或 rollback 会被忽略。
- 现象:内层抛异常,外层仍执行
commit,数据部分写入 - 原因:底层用的是 PDO 的
beginTransaction(),PDO 本身不支持嵌套事务(MySQL 也不原生支持) - 解决方案:避免逻辑嵌套,把所有操作收拢到一个顶层
Db::transaction()闭包中 - 若必须分层封装,改用
Db::startTrans()并手动管理状态变量,靠业务逻辑判断是否该回滚
事务中查不到刚插入的数据?注意读写分离和事务隔离级别
在事务内执行 Db::table('user')->insert($data) 后立刻 select 查不到,常见于开启读写分离或 MySQL 默认隔离级别为 REPEATABLE READ 的场景。
- 读写分离时,
insert走写库,select可能被路由到从库(未同步),需强制指定主库:Db::master()->table('user')->select() - 即使单库,
REPEATABLE READ下事务快照可能不包含本事务未提交的变更,但 ThinkPHP 默认会复用连接,一般不影响;若用了Db::connect()新建连接,则不在同一事务上下文 - 验证方式:在事务闭包末尾加
throw new \Exception('test');,看数据是否消失——消失说明事务生效,没消失说明根本没进事务
事务回滚后连接还可用?别假设连接自动重置
事务回滚不会重置 PDO 连接状态,某些错误(如死锁、超时)可能导致连接进入不可用状态,后续查询直接报 PDOException: SQLSTATE[HY000]: General error。
- 现象:回滚后紧接着执行新查询,报错“Cannot execute queries while other unbuffered queries are active”
- 原因:上一个查询结果集未释放,或事务内部有未 fetch 完的
SELECT游标 - 安全做法:事务结束后,显式调用
Db::close()(仅当确定不再用该连接);更推荐复用连接但确保每个查询完整执行(如->select()后不保留未遍历的Cursor) - 生产环境建议:监控
db.query_time和db.error日志,回滚高频出现时检查是否有长事务或锁竞争
事务不是开关,是上下文。闭包里每行代码都在同一个连接、同一个快照、同一个异常链路上——断哪一环,整条链就失效。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











