Hyperf跨协程事务回滚失败的根本原因是PDO连接被跨协程复用,导致子协程操作的是新连接而非原事务连接;必须通过Context透传同一PDO实例并显式绑定,且多库需分别处理回滚。

Hyperf跨协程事务为什么回滚不了
根本原因不是事务本身失效,而是PDO连接实例被跨协程复用了。Hyperf的DB操作默认绑定在当前协程上下文里,Db::connection()返回的连接对象只在创建它的协程内有效;一旦用go()启动新协程,并在其中调用Db::beginTransaction()或Db::rollback(),实际操作的是另一个协程里的新连接,和原事务完全无关。
常见现象包括:
- 主协程开启事务后启子协程执行写操作,主协程调
Db::rollback()但子协程数据已落库 - 日志显示“Rollback executed”,但查表发现变更仍在
-
SHOW PROCESSLIST里看到多个处于Sleep状态的连接,但事务状态不一致
必须用协程上下文透传Connection实例
不能靠Db::connection('default')在子协程里“重新获取”连接——那是个全新连接。正确做法是把主协程中已开启事务的连接实例,通过协程上下文(Context)显式传递进去。
实操要点:
- 在主协程开启事务后,用
Db::connection()->getPdo()拿到底层PDO实例,存入上下文:Context::set($cid, 'pdo', $pdo) - 子协程启动时,从上下文取出该
PDO,再用Db::connection()->setPdo($pdo)强制复用 - 所有
Db::table()、Model::save()等操作都必须基于这个复用连接,否则仍走默认连接池 - 事务提交/回滚必须由主协程统一控制,子协程只负责业务逻辑,不调
commit()/rollback()
示例关键片段:
// 主协程
$pdo = Db::connection()->getPdo();
Context::set($cid, 'shared_pdo', $pdo);
go(function () use ($cid) {
$pdo = Context::get($cid, 'shared_pdo');
Db::connection()->setPdo($pdo);
User::create(['name' => 'from coroutine']);
});
Db::rollback(); // 此处才能真正回滚子协程写入
跨库事务更得手动管好每个连接
如果涉及Db::connection('log')和Db::connection('user')两个库,每个库的连接都要单独透传。Hyperf不会自动关联多连接事务,Db::rollback()只作用于default连接,其他连接必须显式调用Db::connection('xxx')->rollback()。
容易漏掉的点:
- 忘记为每个连接设置
setPdo(),导致部分库走默认连接池 - 子协程里混用
Db::table()和Db::connection('xxx')->table(),后者可能没绑定共享PDO - 异常未覆盖全部连接的回滚路径,比如
catch里只写了Db::rollback(),漏了Db::connection('log')->rollback()
比透传更稳的方式:避免跨协程事务
绝大多数业务场景其实不需要在子协程里操作事务主体。真正需要并发写的,应该拆成“主协程事务 + 子协程异步通知”,比如:
- 主协程完成核心数据变更并提交
- 再用
go()发消息到Redis/Kafka,由消费者处理日志、推送、统计等非关键路径 - 若必须强一致性,改用分布式事务方案(如Seata),而不是硬扛协程间事务透传
强行透传PDO虽可行,但极易因上下文生命周期管理不当(比如协程提前结束导致PDO被销毁)引发PDOException: SQLSTATE[HY000]: General error这类底层错误,排查成本远高于架构调整。











