ThinkPHP 5.1+ 事务必须在同一 Db 实例或模型上调用,禁止跨模型混用 startTrans/commit/rollback;推荐使用 Db::transaction() 闭包写法,但需手动 try-catch 处理异常以确保回滚。

ThinkPHP 5.1+ 的事务写法统一了,但 startTrans/commit/rollback 不能跨模型调用
TP 5.1 到 6.x 默认都用 Db 类或模型的静态方法管理事务,语法表面一致,实际行为差异藏在底层驱动和连接复用逻辑里。最常踩的坑是:在一个模型里 startTrans,然后在另一个模型里直接 commit —— 这在 MySQL 下可能成功,但在 PostgreSQL 或 SQL Server 下大概率静默失败,因为事务上下文没绑定到同一连接实例。
实操建议:
- 始终在同一个
Db实例或同一个模型类上调用事务方法,比如统一用Db::startTrans()开启,后续也只用Db::commit() - 避免混用
UserModel::startTrans()和OrderModel::commit(),即使它们用的是同一个数据库配置 - TP 6.0+ 支持
Db::transaction()闭包写法,更安全,推荐优先使用:Db::transaction(function () { User::create([...]); Order::create([...]); });
TP 5.0 的 Transaction 类已废弃,硬切到 5.1+ 会触发 Call to undefined method
TP 5.0 里有独立的 think\facade\Transaction 类,但 5.1 彻底移除,所有事务控制收归 Db 和模型。升级时如果代码里还留着 Transaction::start(),运行就报错。
常见错误现象:
立即学习“PHP免费学习笔记(深入)”;
- 升级后首页白屏,日志里出现
Fatal error: Call to undefined method think\facade\Transaction::start() - CI/CD 流水线通过,但本地测试报错——因为有些旧扩展或第三方包偷偷引用了已删类
实操建议:
- 全局搜索项目代码里的
Transaction::、use think\facade\Transaction,替换成Db::相关调用 - 检查 composer.lock 里是否有依赖 TP 5.0 的旧版扩展(如某些支付 SDK),它们可能不兼容 5.1+
- TP 5.0 的手动事务写法:
Db::name('user')->startTrans(),这种在 5.1+ 仍可用,但语义上已不推荐
TP 6.x 的 Db::transaction() 默认不捕获异常,得自己 try-catch
很多人以为 Db::transaction() 像 Laravel 那样自动 rollback 异常,其实不是。它只是把闭包里所有 DB 操作包进一个事务,但抛出异常后不会自动 rollback,除非你显式处理。
使用场景:
- 需要自定义回滚前的日志记录
- 部分操作失败时想 fallback 到补偿逻辑,而非直接中断
实操建议:
- 必须手动包裹
try/catch,否则异常一出,事务挂起,连接可能被锁住:try { Db::transaction(function () { User::update(['status' => 1]); throw new Exception('fail'); }); } catch (\Exception $e) { // 此时事务已 rollback,但你需要在这里做清理或通知 } - 不要依赖
Db::transaction()的返回值判断成败——它只在闭包执行成功时返回闭包返回值,异常时直接抛出 - 如果闭包里调用了其他含 DB 操作的方法(比如 service 层函数),确保它们没私自开启新事务,否则会嵌套失败
跨库事务在 TP 全系都不支持,别试 Db::connect('db1')->startTrans() + Db::connect('db2')->commit()
TP 所有版本的事务机制都基于单连接实例,所谓“跨库”只是切换了连接配置,但每个 Db::connect() 返回的是独立连接对象,彼此事务上下文完全隔离。强行组合只会让第一个库 commit,第二个库还在 pending 状态,最终数据不一致。
性能与兼容性影响:
- MySQL 8.0+ 的 XA 事务 TP 官方不封装,得手写
XA START语句,且驱动层需启用mysqlnd - SQLite 根本不支持多连接事务,TP 在 SQLite 下调用
startTrans实际是模拟的,跨表也不可靠
实操建议:
- 业务上真要跨库一致性,改用消息队列 + 最终一致性,别硬扛分布式事务
- 同一库内多表操作,放心用
Db::transaction(),这是 TP 最稳的用法 - 如果非要用多个数据库配置,至少保证它们指向同一个物理库(不同 schema),再用
Db::name('schema1.table')显式指定



















