Hyperf中Deadlock报错根源在MySQL死锁,需开启innodb_print_all_deadlocks=ON并查error.log定位;关键看WAITING FOR与HOLDS THE LOCK(S)是否闭环,结合事务SQL反推业务层多表操作顺序不一致问题。

Hyperf 项目中出现 Deadlock found when trying to get lock 报错,基本可以确定是 MySQL 层面发生了死锁,而非框架本身问题。Hyperf 的协程 MySQL 客户端(如 hyperf/db 或 hyperf/database)只是透传了 InnoDB 返回的错误码 1213。排查必须下沉到 MySQL 日志和事务行为本身。
怎么看 Hyerf 报错对应的 MySQL 死锁日志
Hyperf 应用抛出的异常里通常只含错误码和一句话,比如:SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction。它不带上下文,所以不能靠应用日志定位根源。
真正关键的日志在 MySQL 的错误日志(error.log)里,前提是已开启记录:
-
innodb_print_all_deadlocks = ON必须配置在my.cnf中并重启,或运行SET GLOBAL innodb_print_all_deadlocks = ON(临时生效) - 确认
log_error路径可写,例如/var/log/mysql/error.log - 不要依赖
SHOW ENGINE INNODB STATUS\G—— 它只保留最后一次死锁,线上高频发生时极易被覆盖
查日志时直接搜索 DEADLOCK 或 deadlock(大小写不敏感),定位到类似这样的段落:
*** (1) TRANSACTION: TRANSACTION 12345678, ACTIVE 0 sec starting index read ... *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 102 page no 5 index `PRIMARY` of table `db`.`orders` ... *** (2) TRANSACTION: TRANSACTION 87654321, ACTIVE 0 sec updating or deleting ... *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 102 page no 5 index `PRIMARY` of table `db`.`orders` ... *** WE ROLL BACK TRANSACTION (1)
注意看 WAITING FOR 和 HOLDS THE LOCK(S) 是否构成闭环——这是死锁成立的铁证。
如何从死锁日志反推 Hyerf 业务代码问题
死锁日志末尾的 query id 对应的 SQL 就是“最后一根稻草”,但单看一条 SQL 很难复现。你需要结合事务完整行为来判断:
- 事务 A 执行了
UPDATE orders SET status = 'paid' WHERE id = 123(持有了orders主键锁) - 事务 B 同时执行了
UPDATE users SET balance = balance - 100 WHERE id = 456(持有了users主键锁) - 然后事务 A 又试图执行
UPDATE users SET ... WHERE id = 456(等待 B 的锁) - 事务 B 又试图执行
UPDATE orders SET ... WHERE id = 123(等待 A 的锁)
这种交叉顺序在 Hyperf 里常见于:
- 多个
@Transactional方法嵌套调用,且各自更新不同表 - 协程并发调用两个 DAO 方法,一个先改订单、再改用户;另一个反之
- 使用
DB::transaction()手动开启事务,但内部逻辑未统一资源访问顺序
重点检查:是否所有涉及多表更新的事务,都严格按「先 orders → 再 users → 再 inventory」这类固定顺序执行?只要有一个分支顺序不同,就可能触发死锁。
为什么慢查询日志也得一起看
死锁不是孤立事件。很多看似“随机”的死锁,背后是某条 SQL 执行过慢,把锁持有时间拉长,放大了竞争窗口。
- 开启慢查询:
slow_query_log = ON+long_query_time = 0.5(建议设低一点,捕获潜在隐患) - 特别关注出现在死锁时间点前后 1–2 秒内的慢 SQL,尤其是
SELECT ... FOR UPDATE、UPDATE ... WHERE没走索引、或扫描行数超万的语句 - 用
EXPLAIN验证这些 SQL 是否命中索引;没命中的要补联合索引,比如WHERE user_id = ? AND status = ?就该建idx_user_status
一个典型陷阱:UPDATE account SET balance = ? WHERE phone = ? —— 如果 phone 没索引,会锁整张表或大量页,极大提高死锁概率。Hyperf 里容易忽略 DAO 层 SQL 的执行计划。
Hyperf 侧能做的防御性措施
MySQL 层面无法完全杜绝死锁(InnoDB 自身检测机制也有成本),Hyperf 应用层必须有兜底:
- 捕获
Hyperf\Database\Exception\QueryException,检查$e->getCode() === '40001'或$e->getPrevious()->getCode() === 1213 - 实现指数退避重试(最多 3 次),避免瞬间重试加剧竞争:
usleep(100000 * pow(2, $attempt)) - 禁止在事务内做 HTTP 请求、Redis 调用、文件读写等耗时操作——这些会显著延长锁持有时间
- 用
DB::select('SELECT ... FOR UPDATE NOWAIT')替代默认阻塞式加锁(MySQL 8.0+),让冲突立刻失败而非等待,便于快速识别热点
最易被忽略的一点:Hyperf 的 go() 协程并发下,多个请求共享同一个数据库连接(PDO 实例)是危险的。务必确认你用的是支持协程化的 MySQL 客户端(如 swoole/mysql 或 hyperf/db 的协程驱动),否则连接复用会引发不可预知的锁状态污染。


















