Hyperf 中 DB::beginTransaction() 不能直接调用,因协程环境下连接复用会导致事务混乱;必须使用 DB::transaction() 确保协程隔离与自动提交/回滚,锁操作也须在事务内生效。

Hyperf 中 DB::beginTransaction() 为什么不能直接用
直接调用 DB::beginTransaction() + DB::commit()/DB::rollback() 在 Hyperf 协程环境下极易出错,核心原因是连接复用失控。协程共享同一个 PHP 进程,但每个协程应独占一个数据库连接;而手动开启事务后若未及时提交或回滚,该连接可能被其他协程误取并复用,触发 MySQL server has gone away (2026) 或数据错乱。
典型错误场景包括:
- 协程 A 调用
DB::beginTransaction(),执行部分 SQL 后挂起(如 await HTTP 请求) - 协程 B 从连接池取出同一连接,发现已有未结束事务,执行自己的 SQL,破坏原子性
- 协程 A 恢复后继续操作,最终
DB::commit()实际提交的是混合状态
DB::transaction() 是唯一推荐的入口
DB::transaction() 内部做了协程隔离:它会为当前协程绑定专属连接、自动管理生命周期,并在回调退出时强制 commit 或 rollback,无论是否异常。这是 Hyperf 官方明确要求的写法。
正确用法只有这一种:
use Hyperf\DbConnection\Db;
DB::transaction(function () {
DB::table('users')->insert(['name' => 'Alice']);
DB::table('orders')->insert(['user_id' => 1, 'amount' => 100]);
// 无需 try-catch —— 异常会自动 rollback 并 re-throw
});
注意:
- 闭包内不要调用
DB::beginTransaction(),否则覆盖内部机制 - 闭包外调用
DB::commit()或DB::rollback()无效且危险 - 嵌套事务不被支持 —— 第二层
DB::transaction()会报错或静默降级
需要局部回滚?只能手动加 SAVEPOINT
Laravel 和 Hyperf 的 DB::transaction() 不自动创建保存点,也无法用 DB::rollBackTo() 回滚到中间状态。如果业务需要“主流程成功,子流程失败可忽略”,必须手写 SQL 级保存点。
示例(MySQL):
DB::transaction(function () {
DB::statement('SAVEPOINT before_notification');
try {
sendEmail();
} catch (\Exception $e) {
DB::statement('ROLLBACK TO SAVEPOINT before_notification');
// 继续执行后续逻辑,如记录日志
}
DB::table('logs')->insert(['status' => 'done']);
});
关键约束:
-
SAVEPOINT名称需唯一且有意义,避免重复命名导致覆盖 - PostgreSQL 和 MySQL 语法一致,SQLite 支持但不建议用于生产
- 不能跨
DB::transaction()闭包使用保存点 —— 事务结束即销毁所有 savepoint
协程中锁必须和事务共存
在 Hyperf 里,->lockForUpdate() 或 ->sharedLock() 只有在事务内才生效。单独在 model 查询中加锁,不包裹在 DB::transaction() 里,等同于无锁 —— 数据库不会持有行锁,协程间仍可并发修改。
正确姿势:
DB::transaction(function () {
$user = User::where('id', 1)->lockForUpdate()->first(); // ✅ 锁生效
$user->balance -= 100;
$user->save();
});
常见误用:
- 先查再锁:查完再进事务,中间窗口期已被其他协程修改
- 锁在事务外:SQL 执行完连接释放,锁立即失效
- 锁粒度太大:全表锁
LOCK TABLES在 Hyperf 中基本不可用,会阻塞整个连接池
事务边界就是锁的有效边界,这点在高并发扣库存、抢购类场景里特别容易被忽略 —— 锁没锁住,不是语法问题,是结构问题。


















