死锁根本原因是多个事务以不一致顺序访问相同数据行并持有互斥锁,与协程无关;Hyperf协程仅放大并发抢锁概率,需统一锁序、显式事务控制、捕获1213错误重试,并启用innodb_print_all_deadlocks定位。

不会单纯因为“在 Hyperf 协程里查 MySQL”就导致死锁。
死锁是数据库层面的资源竞争现象,和应用是否用协程、线程无关。Hyperf 的协程只是让 PHP 代码能并发、轻量地发起 SQL 请求,但最终执行 SQL、加锁、事务控制,全由 MySQL 的 InnoDB 存储引擎决定。
真正引发死锁的,是多个事务以不一致的顺序访问相同的数据行(或索引记录),并持有互斥锁(如 SELECT ... FOR UPDATE 或 UPDATE/DELETE),最终形成环形等待。
常见触发死锁的场景(与协程无关,但在协程密集调用时更容易暴露)
交叉更新不同表的同一行
事务 A 先更新users表某行,再更新orders表某行;
事务 B 反过来先更新orders,再更新users—— 两个事务可能互相等待对方释放锁。范围锁 + 并发插入/更新
比如UPDATE user SET status=1 WHERE age > 25,InnoDB 可能锁住多个索引区间;高并发下不同事务锁住重叠但不完全相同的记录集,容易形成死锁。未提交事务长时间挂起
协程中开启事务后,因逻辑阻塞、异常未 catch、或忘记commit/rollback,导致锁长期持有,增大与其他事务冲突概率。使用
SELECT ... FOR UPDATE且顺序不可控
尤其当业务根据动态条件(如用户 ID 列表乱序)批量加锁,不同请求锁行顺序不一致,死锁风险显著上升。
Hyperf 协程环境下的关键注意事项
✅ 连接池隔离有效降低干扰
Hyperf 默认使用mysql连接池(如hyperf/database+hyperf/db),每个协程从池中获取独立连接,事务彼此隔离,不会因协程共享连接而“传染”死锁。⚠️ 不要在协程中复用长生命周期的 PDO 实例
手动 new PDO 或跨协程传递连接对象,可能造成事务上下文错乱、锁状态残留,间接诱发异常行为。-
✅ 推荐显式控制事务边界
$this->db->transaction(function () { $this->db->table('users')->where('id', 1)->lockForUpdate()->first(); $this->db->table('logs')->insert(['user_id' => 1, 'action' => 'update']); });避免手动
beginTransaction()后忘记收尾,这是协程环境下最易被忽略的隐患。 -
✅ 捕获死锁异常并自动重试(强烈建议)
MySQL 返回错误码1213(Deadlock found when trying to get lock)时,Hyperf 可结合Retry组件或自定义装饰器重试:try { $this->db->transaction($callback); } catch (QueryException $e) { if ($e->getCode() === 'HY000' && $e->getPrevious()?->getCode() == 1213) { // 重试一次 $this->db->transaction($callback); } }
如何快速定位和验证
-
查看 MySQL 最近一次死锁:
MySQL8 CRM Schema Design Expert下载面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
SHOW ENGINE INNODB STATUS\G
关注
LATEST DETECTED DEADLOCK部分,明确哪两条 SQL、哪个事务、锁了哪些行。 -
开启全量死锁日志(临时调试用):
SET GLOBAL innodb_print_all_deadlocks = ON;
日志会写入 MySQL 错误日志,方便分析高频死锁模式。
死锁不是协程的锅,而是数据访问逻辑和事务设计的问题。Hyperf 协程本身是安全的,反而因其高并发特性,能更快暴露出原本低流量下不易发现的数据库竞争缺陷。重点放在 SQL 访问顺序统一、事务粒度合理、异常兜底重试上,就能稳住。










