hyperf 中 db::transaction() 不安全,因其静态门面未绑定协程上下文,易导致事务跑错连接、回滚失效、连接泄漏;必须改用 connection::transaction() 或显式获取并释放协程专属连接。

Hyperf 中直接套用 Laravel 风格的 DB::transaction() 或手动 beginTransaction()/commit() 会大概率导致事务不生效、连接泄漏、数据脏写——根本原因不是语法错,而是静态门面未绑定协程上下文,事务操作可能跑在错误连接上。
DB::transaction() 在 Hyperf 里为什么不安全
它本质是静态门面调用,底层未做协程上下文绑定。同一 Worker 进程中多个协程并发执行时,DB::beginTransaction() 可能从连接池拿到 A 协程的连接,但 DB::commit() 却作用于 B 协程持有的连接句柄,结果就是:A 的事务没提交,B 的连接被误释放,甚至触发 MySQL server has gone away。
- 现象:刚
insert就select查不到;并发下部分请求事务回滚失败;日志里反复出现连接超时或空结果 - 根源:Hyperf 的连接池按协程隔离,但
DB::静态调用绕过了协程感知逻辑 - 替代方案必须显式使用连接实例,而非静态门面
正确写法:用 Connection::transaction() 绑定当前协程
Hyperf\Database\Connection 提供了协程安全的事务封装,内部自动从当前协程上下文获取连接、执行、归还,无需手动 begin/commit。
- 必须通过 DI 容器获取连接实例,例如
private Connection $connection;+@Inject - 不要 new Connection(),它依赖容器注入的配置和连接池管理
- 回调函数内所有 DB 操作都自动复用该连接,异常时自动 rollback
$this->connection->transaction(function () {
User::create(['name' => 'alice']);
Notification::create(['user_id' => 123]);
// 抛异常则自动 rollback
});
需要手动控制事务时的三步铁律
某些场景(如嵌套事务、保存点、跨服务协调)必须手动管理。此时务必遵循:获取连接 → 绑定到当前协程 → 显式释放。
- 第一步:用
$this->connection->getPdo()或$this->connection->getRawConnection()获取协程专属连接 - 第二步:所有 SQL 必须通过该连接实例执行,禁止混用
DB::静态调用 - 第三步:无论成功或异常,必须在
finally块中调用$this->connection->release()归还连接(Hyperf 3.1+ 推荐)
$pdo = $this->connection->getRawConnection();
try {
$pdo->beginTransaction();
$pdo->exec("INSERT INTO logs (...) VALUES (...)");
$pdo->commit();
} catch (\Exception $e) {
$pdo->rollback();
throw $e;
} finally {
$this->connection->release(); // 关键!否则连接滞留
}
SAVEPOINT 和嵌套事务的协程陷阱
Hyperf 不支持传统意义上的嵌套事务,SAVEPOINT 必须由同一连接实例管理,且不能跨协程复用。
- 错误:在中间件设
SAVEPOINT a,控制器里再ROLLBACK TO a—— 两者可能不在同一连接上 - 正确:所有
SAVEPOINT相关语句必须在同一个Connection::transaction()回调内完成 - 若需跨方法协作,把连接实例作为参数透传,而不是依赖全局状态或 static 缓存
最易被忽略的是连接归还时机:事务结束 ≠ 连接自动释放。Hyperf 的连接池不会因事务结束就回收连接,必须显式 release() 或等协程退出后由框架兜底(但不可依赖兜底,压测下极易耗尽)。











