Hyperf并发死锁根源在于锁未绑定协程生命周期、事务未压缩、资源访问顺序不一致三者叠加;正确流程为先加锁→再开事务→执行DB操作→提交事务→最后在finally中释放锁。

Hyperf 里并发锁竞争死锁,根本不是“锁用得不对”,而是锁没绑定到协程生命周期、事务没压缩、资源访问顺序不一致——三者叠加,协程一并发就爆。
为什么 DB::transaction() 包着 RedisLock 还会死锁
常见错误是把 RedisLock 放在 DB::transaction() 外层,或反过来嵌套。结果就是:锁拿到了,但事务还没提交,数据库行锁一直挂着;或者事务已提交,锁却还卡着没释放,其他协程全在等它。
- Redis 分布式锁只管“谁先抢到”,不管“DB 是否已写完”——它和事务不是原子联动的
- 正确顺序必须是:先加锁 → 再开事务 → 执行 DB 操作 → 提交事务 → 最后释放锁
- 千万别在事务块里调
$lock->release(),一旦 rollback,锁可能提前释放,导致并发写入 - 释放锁必须放在
finally块里,且确保只执行一次:if ($acquired) { $lock->release(); }
LockInterface 注入失败导致连接复用混乱
直接 new RedisLock($redis) 看似能用,但高并发下会出现锁失效、连接耗尽、甚至 Connection refused 错误,因为绕过了 DI 容器管理的连接池。
- 检查
config/autoload/dependencies.php是否注册了Hyperf\Lock\LockInterface→Hyperf\Lock\Driver\RedisDriver - 控制器或服务中必须用构造器注入:
public function __construct(protected LockInterface $lock) {},而非手动 new - 若用
make(LockInterface::class),需确认当前协程上下文是否支持容器解析(定时任务/异步队列中可能失效) - 锁超时时间(
expire)建议设为 5–10 秒,必须短于事务最大执行时间,否则锁过期而事务未完成,等于没锁
协程间锁变量污染导致“抢到锁却不是自己”
用 static $lock 或单例服务属性存锁实例,会导致 A 协程 set 的 lockKey 被 B 协程读取并误释放——这不是锁机制问题,是状态没隔离。
- 锁 key 必须带协程唯一标识:
'order_lock_' . Context::get('request_id', 'unknown'),不能硬编码或用uniqid()(不同协程可能撞) - 不要在单例服务里定义
protected $lockKey,每次加锁都应生成新 key 并传入 - Lua 释放脚本里的 token 必须是当前协程生成的随机值,且只在本次加锁时使用,不可复用
- 用
Context::set('lock_token', $token)存 token 比存在$this->token安全得多,子协程若需释放,必须显式透传
死锁日志看不到?先查 MySQL 配置漏没漏
没开 innodb_print_all_deadlocks = ON,MySQL 就不会把死锁现场写进 error log,你只能靠猜——而 Hyperf 协程环境下,猜错成本极高。
- 确认配置已生效:
mysql -e "SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';",返回ON才算成功 - 同时检查
log_error_verbosity = 3,否则死锁详情会被截断 - error log 路径通常为
/var/log/mysql/error.log,不是应用日志目录 - 死锁日志里重点看两段:
*** (1) TRANSACTION和*** (2) TRANSACTION中的 SQL 与锁等待行,它们暴露了哪两条语句在争哪两个主键
真正麻烦的从来不是“怎么加锁”,而是锁和事务、协程上下文、Redis 连接生命周期这四者的边界没划清。一个 $this->lock 属性,一条没设 PX 的 set,一次没 Context::copy() 的子协程调用,都可能让锁变成并发放大器。











