Hyperf中数据库死锁根本原因是事务锁资源顺序不一致与持有时间过长,协程仅放大并发抢锁概率;需启用innodb_print_all_deadlocks、统一主键驱动、显式事务超时及索引优化来根治。

Hyperf协程环境下,UPDATE 或 SELECT ... FOR UPDATE 引发的数据库死锁,根本原因不是“用了协程”,而是事务在并发中锁资源顺序不一致 + 持有时间过长。直接杀进程或重启服务治标不治本,必须从 SQL 执行路径和事务生命周期入手。
为什么Hyperf里死锁更隐蔽?
Hyperf 的协程调度不会自动打断正在执行的 SQL 事务 —— 即使你开了 100 个协程并发调用同一个转账接口,每个协程仍会独立开启事务、加锁、等待、回滚。而协程轻量、启动快、复用线程,反而放大了“同一秒内多个事务交叉抢锁”的概率。常见表现是:Deadlock found when trying to get lock 错误突增,但日志里看不到明显慢查询或长事务记录。
- 协程不等于事务自动短,
DB::transaction()块内哪怕只有一条UPDATE,只要没提交,锁就一直挂着 - Hyperf 默认未开启
innodb_print_all_deadlocks=ON,MySQL 不会把死锁详情写进 error log,只能靠业务层捕获异常后人工反查 - 使用
where name = ?更新却没给name加索引?InnoDB 会升级为表锁,所有并发更新全卡住
如何定位死锁发生的具体 SQL 和索引路径?
别猜,直接让 MySQL 把死锁现场吐出来。在 MySQL 配置中启用:
innodb_print_all_deadlocks = ON log_error_verbosity = 3
然后复现一次死锁,立刻查 error log(路径通常为 /var/log/mysql/error.log),你会看到类似这样的输出:
*** (1) TRANSACTION: TRANSACTION 123456789, ACTIVE 0 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 1001, OS thread handle 140234567890123, query id 2000 localhost root updating UPDATE account SET balance = balance - 100 WHERE id = 1 <p>*** (2) TRANSACTION: TRANSACTION 123456790, ACTIVE 0 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 1002, OS thread handle 140234567890124, query id 2001 localhost root updating UPDATE account SET balance = balance + 100 WHERE id = 2</p><p>*** WE ROLL BACK TRANSACTION (2)
关键看两行:starting index read 表明它正在走哪个索引;WHERE id = ? 说明走了主键;如果出现 WHERE name = ? 且没索引,就会显示 table scan 或锁住 PRIMARY + GEN_CLUST_INDEX,这就是隐患点。
减少长事务:Hyperf 中必须做这三件事
Hyperf 的 DB::transaction() 默认无超时,一个协程卡住,整个 worker 进程可能被拖慢。必须主动控制生命周期:
- 用
DB::transaction(..., $timeout)显式传入超时秒数,例如DB::transaction(fn () => {...}, 3),超时自动 rollback,避免锁无限期持有 - 禁止在事务块内做任何 IO 密集操作(如调第三方 HTTP、写文件、sleep),这些会让锁持有时间不可控;应拆成“查 → 处理 → 再查 → 更新”多步,用最终一致性补偿
- 对批量操作,改用分页 + 循环事务,例如每次只处理 100 条,而不是把 10 万条塞进一个事务;Hyperf 的
Coroutine::sleep(0.001)可插在批次之间,释放调度权
优化索引执行顺序:避免“先普通索引再主键”式死锁
MySQL InnoDB 的锁获取顺序依赖执行计划。以下两种写法看似等价,但锁顺序相反,极易引发死锁:
-- 场景 A:走 name 索引,再回表查主键 UPDATE user SET status = 'paid' WHERE name = 'alice'; <p>-- 场景 B:走主键,再查 name 字段(如果需要) SELECT * FROM user WHERE id = 123 FOR UPDATE;
当 A 和 B 并发执行时,A 先锁 name 索引项,再申请主键锁;B 先锁主键,再申请 name 索引锁 —— 死锁闭环形成。解决方法只有一个:统一按主键驱动:
- 所有更新/删除语句,尽可能用
WHERE id = ?,避免依赖非唯一普通索引 - 如果必须用
name查询,确保name是唯一索引(UNIQUE KEY),这样 InnoDB 能直接定位到唯一行,避免间隙锁扩散 - 在 Hyperf 的 Repository 或 Service 层封装一个
findByUniqueKey()方法,内部先SELECT id FROM ... WHERE name = ?,再用该id做后续更新,彻底解耦查询与修改路径
最易被忽略的一点:即使你代码里只写了 WHERE id = ?,如果表上没有主键(比如用 UUID 当主键但没设 PRIMARY KEY),InnoDB 会自建隐藏聚簇索引,导致锁行为不可预测。上线前务必确认 SHOW CREATE TABLE xxx 中存在明确的 PRIMARY KEY 定义。


















