Hyperf异步队列数据库死锁根因是协程放大锁竞争且事务未统一资源访问顺序;需启用innodb_print_all_deadlocks、强制主键驱动、压缩事务粒度、显式超时与重试机制。

Hyperf异步队列在高并发订单处理、批量推送等场景中频繁触发数据库死锁,错误日志里反复出现“Deadlock found when trying to get lock”,但事务逻辑看似正确,排查时却发现MySQL error log里连死锁详情都没有——根本原因是协程放大了锁竞争,而事务设计本身没遵循资源访问顺序一致性。
统一锁资源获取顺序
所有涉及多行更新或SELECT FOR UPDATE的业务,必须按相同字段顺序加锁。例如账户余额扣减与流水记录插入,若一个事务先锁用户ID再锁订单ID,另一个事务反向操作,死锁必然发生。
检查当前SQL执行路径:用EXPLAIN分析每条UPDATE/SELECT FOR UPDATE语句,确认其实际走的索引和扫描范围。如果WHERE条件未命中索引,InnoDB会升级为表锁,所有并发更新全部阻塞。
强制主键驱动:所有UPDATE必须带主键条件(如WHERE id = ?),禁止使用非索引字段(如WHERE status = 'pending')触发间隙锁或全表扫描。
【关键前提】启用innodb_print_all_deadlocks = ON并重启MySQL,否则死锁现场永远不输出到error log,你只能靠猜。
缩短事务生命周期
第一步:把DB::transaction()块压缩到最小粒度,只包裹真正需要原子性的SQL段。不要把HTTP参数校验、Redis缓存读写、日志记录等无关操作塞进事务里。
第二步:避免在事务内调用外部HTTP接口或执行sleep()。协程不会自动中断正在执行的SQL事务,哪怕你加了Coroutine::sleep(0.1),锁也一直挂着。
第三步:对长耗时任务拆分——比如批量更新1000条记录,改用循环+单条事务提交,每次只处理50条并commit,而不是包在一个大事务里执行到底。
这一步操作起来很简单,直接把原事务块里的非DB操作剪切出来放到外面就行。
配置显式事务超时与重试策略
方法一:在config/autoload/database.php中为对应连接设置'timeout' => 5(秒级),同时开启'sticky' => false,防止连接复用导致事务状态残留。
方法二:在Job类handle()方法开头手动设置PDO超时:
DB::connection()->getPdo()->setAttribute(PDO::ATTR_TIMEOUT, 3);
方法三:启用Hyperf AsyncQueue的失败重试机制,在src/async-queue/publish/async_queue.php中配置:
'retry_seconds' => 60,
'max_attempts' => 3,
并确保dead_letter_queue已启用,避免失败任务无限堆积。
【不可逆操作】上线前必须验证:将max_attempts设为1,人为制造一次死锁,确认任务能进入失败队列而非卡死在processing状态。


















