Db::startTrans不会自动回滚,需手动配对commit/rollback;推荐封装transaction函数实现异常自动回滚;混用模型与Db时需确保同一连接;ThinkPHP不支持嵌套事务,savepoint需直接操作PDO。

事务没回滚,Db::startTrans 后抛异常为啥不生效
ThinkPHP 的 Db::startTrans 默认不会自动捕获异常并回滚,它只是开启事务,后续全靠手动控制。很多人写了 try...catch 却忘了在 catch 里调 Db::rollback,或者压根没写 catch,导致异常后事务悬空、数据不一致。
常见错误现象:Db::startTrans() 后执行 SQL 报错(比如字段不存在、唯一键冲突),但数据库里前几条 INSERT/UPDATE 已经提交了。
- 必须显式配对使用
Db::commit()和Db::rollback() - 不能只依赖 PHP 异常终止就“以为”事务会撤回
- 如果用的是 PDO,默认事务模式是
PDO::ATTR_AUTOCOMMIT => true,startTrans只是临时关掉它,异常不会自动触发 rollback
如何让事务在异常时自动回滚(不用手写 try/catch)
ThinkPHP 本身不提供“声明式自动回滚”,但可以封装一个轻量工具函数,把 startTrans + try/catch + commit/rollback 逻辑收拢起来,避免每次重复写样板代码。
使用场景:批量写入、多表关联更新、支付扣减+订单生成等强一致性操作。
立即学习“PHP免费学习笔记(深入)”;
- 推荐封装成闭包执行器,例如
transaction(function () { ... }) - 闭包内所有 DB 操作都在同一事务上下文中,抛出任何未捕获异常都会触发回滚
- 注意:闭包内不能提前 return 或 exit,否则
finally可能不执行;也别在闭包里 throw 新异常覆盖原异常,会丢失错误上下文
function transaction(callable $callback)
{
\think\facade\Db::startTrans();
try {
$result = $callback();
\think\facade\Db::commit();
return $result;
} catch (\Exception $e) {
\think\facade\Db::rollback();
throw $e;
}
}
// 使用
transaction(function () {
\think\facade\Db::table('user')->insert(['name' => 'a']);
\think\facade\Db::table('log')->insert(['msg' => 'fail here']); // 假设这里报错
});
Db::startTrans 在模型和 Query 构建器中混用的风险
事务是连接级的,Db::startTrans 影响的是当前数据库连接实例。但如果在事务中穿插使用模型(UserModel)和原生 Db::table(),只要它们共享同一个连接(默认情况),事务仍然有效。问题出在「连接被切换」或「连接复用混乱」上。
容易踩的坑:
- 模型配置了独立数据库连接(比如
'connection' => 'log_db'),而Db::table()用的是默认连接 —— 这两个事务完全隔离,rollback 只影响其中一个 - 用了读写分离,写操作进了 master 连接,但某个模型查从库(slave)又新建了连接,这个新连接不在事务范围内
- 事务期间调用了另一个服务的数据库操作(比如 Redis 缓存更新失败),这不属于 DB 事务管辖,得靠补偿或本地消息表兜底
嵌套事务(savepoint)在 ThinkPHP 中怎么安全用
ThinkPHP 不原生支持 savepoint,Db::startTrans 多次调用不会创建嵌套事务,而是被忽略或报错(取决于底层驱动)。真要实现局部回滚,得手动用 PDO 的 savepoint 和 rollback to savepoint。
适用场景:主流程不能失败,但某子步骤可降级(比如发通知失败不影响下单)。
- 不要依赖
Db::startTrans嵌套,它不是 savepoint - 若需 savepoint,直接操作 PDO 实例:
\think\facade\Db::getPdo()->exec("SAVEPOINT sp1") - 注意:MySQL 5.7+ 支持 savepoint,但 SQLite、某些 PDO 驱动可能不兼容;且 savepoint 不跨连接,不能在不同
Db::connect()实例间共享 - 更稳妥的做法是拆成独立事务 + 补偿逻辑,而不是强依赖 savepoint











