Db::transaction闭包方式最安全推荐,自动处理开启、提交与回滚:传入匿名函数,所有DB操作写入其中,成功自动commit,抛异常自动rollback。

Db::transaction 是最安全、最推荐的用法,它自动处理开启、提交和回滚,不用手动写 try/catch 框架逻辑。
Db::transaction 闭包方式怎么写
直接传一个匿名函数进去,所有数据库操作写在闭包里,成功自动 commit,抛异常自动 rollback:
Db::transaction(function () {
Db::table('users')->where('id', 1)->update(['balance' => 900]);
Db::table('logs')->insert(['user_id' => 1, 'action' => 'deduct', 'amount' => 100]);
});
注意三点:
- 闭包内任何一句 SQL 报错(比如字段不存在、唯一约束冲突、类型不匹配),整个事务立刻回滚
- 闭包外的代码不受事务影响,哪怕闭包执行完再抛错,也不会触发回滚
- 不要在闭包里调用
Db::commit()或Db::rollback(),会干扰框架内部状态
手动控制事务时 try/catch 容易漏什么
手动用 Db::startTrans() + commit()/rollback() 时,最常踩的坑是没覆盖全部异常路径:
- 只捕获
\Exception,但 PDO 可能抛出PDOException,而它不继承\Exception(PHP 8+ 已统一,但老项目仍需确认) - 忘记在
finally或双层catch中确保rollback()被执行(例如超时、内存溢出等非捕获型中断) - 多个模型操作时,每个模型都得单独调用
startTrans()和对应rollback(),否则只回滚了部分表
正确写法示例(多模型):
立即学习“PHP免费学习笔记(深入)”;
TopicOrderModel::startTrans();
TopicOrderCommentModel::startTrans();
try {
$order = new TopicOrderModel;
$order->status = 4;
$order->save();
$comment = new TopicOrderCommentModel;
$comment->topic_order_id = $order->id;
$comment->content = input('content');
$comment->save(); // 这里若因字段类型不匹配失败,会进 catch
TopicOrderModel::commit();
TopicOrderCommentModel::commit();
} catch (\Throwable $e) {
TopicOrderModel::rollback();
TopicOrderCommentModel::rollback();
throw $e;
}
事务不生效的常见原因
写了 startTrans() 却发现数据还是被写进去了,大概率是以下之一:
- 数据库表引擎不是
InnoDB(MyISAM 不支持事务),检查SHOW CREATE TABLE users确认引擎 - 用了
Db::query()或原生execute()执行 DDL 语句(如CREATE、ALTER),这类语句会隐式提交当前事务 - 事务过程中调用了其他未受控的数据库连接(比如自己 new PDO),它不在 ThinkPHP 的事务上下文里
- 开启了自动提交(
autocommit=1),虽然 ThinkPHP 默认关,但某些中间件或配置可能重置它
嵌套事务和 savepoint 怎么理解
ThinkPHP 支持事务嵌套,底层靠的是 SAVEPOINT,不是真正嵌套。也就是说:
- 外层
rollback()会把所有 savepoint 都清除,整个回滚到底 - 内层
rollback()只回滚到最近一个 savepoint,不影响外层已执行的操作 - 嵌套层级过深(>10 层)可能触发 MySQL 的
max_sp_recursion_depth限制 - 跨请求/跨协程不能共享事务,每次 HTTP 请求都是独立连接,
startTrans()只对当前请求有效
实际开发中,除非明确需要局部回滚(比如批量导入中某条失败不影响其余),否则优先用单层 Db::transaction,更可控。



















