结论:mysql事务在hyperf协程下不回滚,是因pdo连接被协程复用导致事务状态未隔离,rollback()未作用于开启事务的连接;必须使用db::transaction()闭包模式并配合co::yield()绑定协程上下文。

MySQL 事务在 Hyperf 协程环境下不回滚的典型表现
直接说结论:不是事务“失效”,而是 pdo 连接被协程调度器复用后,事务状态未隔离,导致 rollback() 调不到真正开启事务的那个连接。常见现象是——明明写了 try...catch 并调了 $this->db->rollback(),但数据还是写进去了。
Hyperf 3.x 中必须用 Co::yield() 配合 Db::transaction()
Hyperf 默认的 Db::transaction() 是同步封装,底层没做协程上下文绑定,在并发请求中会跨协程污染连接。修复核心是强制让事务生命周期绑定当前协程:
- 不能手动
beginTransaction()+commit()/rollback(),必须走Db::transaction()的闭包模式 - 闭包内所有数据库操作必须使用同一个
Db实例(不要 new Query 或换 connection) - 若事务中混用了
go启动的子协程,且子协程也操作数据库,必须显式传入当前事务连接:Db::connection()->table(...) - 关键补丁:在事务闭包开头加
Co::yield(),确保后续 PDO 操作落在同一协程栈(Hyperf v3.1.10+ 已内置该行为,旧版本需手动加)
// ✅ 正确写法(Hyperf 3.0.32+)
Db::transaction(function () {
Co::yield(); // 强制绑定当前协程上下文
User::create(['name' => 'a']);
throw new RuntimeException('触发回滚');
});
// → 数据不会插入
Db::transaction() 的超时与嵌套事务陷阱
Hyperf 的 Db::transaction() 默认不支持真正的嵌套事务(即 savepoint),连续两次调用会报 There is no active transaction 或静默失败。同时,协程环境下 PDO 连接空闲超时(wait_timeout)可能早于事务执行完成,导致 rollback 时连接已断:
- 检查 MySQL 配置:
wait_timeout建议 ≥ 300,避免事务中途连接被 server 主动断开 - 禁用自动重连:
'options' => [PDO::ATTR_EMULATE_PREPARES => true, PDO::MYSQL_ATTR_INIT_COMMAND => 'SET autocommit=0'],否则rollback()可能因连接重置而丢弃 - 避免在事务中 sleep、http 请求、阻塞 IO;必须做的话,改用
Co::sleep()并确认 DB 连接未被协程调度器回收 - 多表操作务必共用一个
Db实例,不要在事务里用UserModel::query(),应统一用Db::table('users')
验证事务是否真回滚的最小测试代码
别依赖日志或 debugbar,直接查库。以下代码可复现并验证修复效果:
// 在 Command 或 Controller 中运行
$countBefore = Db::table('users')->count();
try {
Db::transaction(function () {
Co::yield();
Db::table('users')->insert(['name' => 'test_rollback', 'created_at' => date('Y-m-d H:i:s')]);
throw new Exception('force rollback');
});
} catch (\Exception $e) {
// 忽略
}
$countAfter = Db::table('users')->count();
var_dump($countBefore === $countAfter); // 必须为 true
如果输出 false,说明事务未回滚,大概率是没走 Db::transaction() 闭包,或中间混用了其他 DB 实例。协程事务的复杂点不在语法,而在连接归属权是否全程可控——这点稍不注意,就变成“看起来回滚了,其实没回滚”。











