事务失效主因是协程切换导致连接更换——Hyperf连接池按协程隔离,跨协程调用必换连接,事务上下文断裂;sleep等同步函数会阻塞协程、致连接被归还,commit发往错误连接。

事务失效不是数据库没提交,而是协程切换后连接被换掉了——Hyperf 的 DB 连接池按协程隔离,跨协程调用必然走新连接,事务上下文就断了。
为什么 Db::transaction() 里嵌 sleep() 就失效
常见错误是想“等几秒再做点事”,于是在事务闭包里写 sleep(1) 或 usleep(100000)。这会阻塞当前协程线程,但 Swoole 调度器无法介入,导致整个协程卡死;更糟的是,如果此时有其他协程抢占执行,原事务所在的 PDO 连接可能已被归还池中,后续操作(比如 commit)发到的已是另一个连接。
-
sleep()、file_get_contents()、curl_exec()等同步函数绝对不能出现在事务闭包内 - 必须用协程安全替代:用
co::sleep(1)(注意是co::sleep,不是sleep) - 即便用了
co::sleep,也要确认它没触发协程切换出当前事务作用域——Hyperf v2.2+ 对事务内协程切换做了增强保护,但低版本仍建议避免在事务中主动让渡控制权
Db::transaction() 外部手动 begin/commit 为什么总报 “MySQL server has gone away”
手动管理事务($this->db->beginTransaction() → 操作 → $this->db->commit())极易出问题,核心在于:你拿到的 $this->db 实例不保证属于当前协程。尤其当它来自构造函数注入、或被静态缓存、或跨 service 传递时,很可能复用的是上一个协程的连接。
- 永远优先用
Db::transaction()闭包方式,它内部会绑定当前协程 ID 并确保全程使用同一连接 - 若必须手动控制(如分阶段提交),请每次从容器重新获取连接:
$connection = make(ConnectionInterface::class),而不是复用类属性或静态变量 - 检查是否在非协程环境(如
Swoole\Timer::tick回调)里调用了 DB 方法——这种场景下Db::transaction()会退化为普通 PDO,失去协程感知能力
事务中调用异步任务或 HttpClient 导致回滚失败
事务闭包里提交一个 PushMessageTask 或发起 HttpClient 请求,看似没问题,但实际埋雷:这些操作本身会启动新协程,而原始事务连接不会自动透传过去。更危险的是,如果 HTTP 请求超时或任务抛异常,事务闭包已退出,Db::transaction() 来不及捕获异常,也就不会触发自动回滚。
- 事务闭包内只做纯 DB 操作,所有外部依赖(HTTP、Redis、消息队列、文件写入)必须挪到事务之外
- 需要“先入库再通知”的场景,用两阶段设计:第一阶段 DB 提交成功后,记录一条待推送状态;第二阶段由独立定时任务或监听器拉取并推送,失败可重试
- 若坚持要在事务中触发异步动作,请用
defer()注册回调(Coroutine::defer()),但它仅保证“协程结束前执行”,不保证在 commit 后、也不保证能捕获 commit 异常,慎用
最易被忽略的一点:事务失效往往不报错,只表现为“数据没写进去”或“查不到刚插入的记录”。这时候别急着查 SQL 日志,先看 SHOW PROCESSLIST 里有没有大量 Sleep 状态连接——那是连接被占住没释放的信号,说明事务没正常结束,大概率卡在了某个阻塞调用或协程泄漏里。











