根本原因是事务上下文不随协程切换自动传递,事务状态绑定于单个协程的连接实例;跨协程调用commit/rollback时操作的是不同连接,导致事务失效。

Hyperf 3.0 中数据库事务在协程间失效,根本原因在于 事务上下文(Transaction Context)未随协程切换自动传递,而底层连接与事务状态绑定在单个协程的执行栈中。
协程内事务依赖连接绑定,不跨协程共享
Hyperf 的 MySQL/PostgreSQL 客户端基于 Swoole 协程驱动,每个协程持有独立的 PDO 或原生连接句柄。当调用 beginTransaction() 时:
- 事务开启操作作用于当前协程持有的连接实例;
-
commit()或rollback()也必须在同一协程内对同一连接实例调用才有效; - 若在协程 A 中开启事务,再
go()启动协程 B 并尝试在 B 中提交,B 拿到的是连接池中新分配(或复用)的连接,该连接并未开启事务 —— 所以commit()实际上什么也没做,甚至可能报错“no transaction is active”。
// ❌ 错误示例:事务跨协程失效
$db->beginTransaction(); // 在主协程开启
go(function () use ($db) {
$db->commit(); // 此处 $db 是新协程里的新实例(或不同连接),事务早已丢失
});
// ✅ 正确做法:所有事务操作必须在同一个协程内完成
go(function () use ($db) {
$db->beginTransaction();
try {
$db->insert(...);
$db->update(...);
$db->commit();
} catch (\Throwable $e) {
$db->rollback();
throw $e;
}
});连接池机制加剧了上下文隔离
Hyperf 的 DbPool 默认按协程 ID 隔离连接(通过 Context::set() / Context::get() 绑定),但事务状态本身不存于 Context,只存在于连接对象内部。即使你手动把连接对象传入子协程,Swoole 协程 MySQL 客户端也不允许跨协程复用连接句柄(底层会触发 Connection is not available in current coroutine 类似错误)。
所以常见误区是:
- 认为
go(fn() => $db->commit())能延续主协程事务 → 实际连接已切换; - 尝试
$connection = $db->getPdo(); go(fn() => $connection->commit());→ 协程不安全,PDO 对象不可跨协程使用。
Eloquent ORM 的事务同样受此约束
即便使用 DB::transaction() 或模型静态方法 User::transaction(),其内部仍是调用 $connection->beginTransaction()。Hyperf 的 Eloquent 已协程化改造,但不改变事务的协程局部性本质。
它保证:
- 同一协程内多次
DB::select()共享事务连接; - 不保证子协程能继承父协程的事务状态。
如何安全实现“类跨协程事务”需求?
实际业务中若需异步协作 + 数据一致性,应规避“跨协程提交”,改用以下模式:
- 使用消息队列(如 Redis Stream、RabbitMQ)将后续操作转为最终一致;
- 用本地事务 + 状态字段(如
status=processing)标记中间态,由定时任务或监听器补全; - 若必须同步等待,就不用
go(),改用协程co::sleep()或直接顺序执行; - 极少数场景可手动管理连接:从连接池取连接 →
set('db.connection', $conn)到当前协程 Context → 显式传参给子协程闭包 → 子协程内复用该连接对象(注意:必须确保该连接未被其他协程并发使用,且生命周期可控)。
本质上,这不是 Hyperf 的缺陷,而是协程模型下资源局部性的合理体现 —— 就像线程里不能共享未加锁的变量一样,协程里也不能默认共享数据库事务状态。


















