死锁发生的核心条件:互斥、持有并等待、不可剥夺、循环等待
关于基础锁(RLock)的死锁风险,主要来自两个层面:
1)客户端层面:如果两个线程互相等待对方持有的锁,且都不释放,就会形成典型的死锁。不过这种情况更多是业务逻辑设计问题,比如lock1和lock2嵌套获取的场景。
2)服务端层面:当Redis连接闪断时,看门狗线程无法续期但业务线程仍在运行,最终锁过期释放,而业务线程在finally块调用unlock时会误删新持有者的锁
🔒 一、基础锁(RLock)死锁场景
- 看门狗续期失败导致锁永久持有
- 发生条件:
- 业务线程持有锁期间,Redis连接发生闪断(如网络波动)
- 看门狗线程因连接异常无法执行续期命令(
pexpire
)
- 结果:
- 锁超时自动释放,但业务线程仍在运行
- 其他线程可获取锁,出现多个线程并发操作临界资源
- 原持有者执行
unlock()
时,因锁已易主,可能误删新持有者的锁
- 嵌套锁导致的循环等待
// 线程1
lockA.lock();
lockB.lock();
...
// 线程2
lockB.lock();
lockA.lock(); // 形成循环等待
- 原理:
若两个线程以相反顺序获取嵌套锁,且恰好在中间步骤阻塞,则形成死锁四要件(互斥、持有等待、不可剥夺、循环等待)
📚 二、读写锁(RReadWriteLock)死锁场景
读锁升级写锁
// 线程1(持有读锁)
rLock.readLock().lock();
rLock.writeLock().lock(); // 阻塞等待所有读锁释放
// 线程2(持有读锁,同样尝试升级)
rLock.writeLock().lock(); // 互相等待对方释放读锁
- 结果:
所有持有读锁的线程都无法升级为写锁,且写锁请求阻塞,导致系统僵死
🔴 三、红锁(RedLock)的死锁风险
时钟漂移引发的锁重叠
- 场景:
- 节点A时钟过快,提前释放锁
- 节点B时钟正常,仍认为锁有效
- 结果:
客户端1在节点A释放锁后,客户端2立即获取锁,但节点B仍保留旧锁数据,导致两客户端同时持有锁
⚖️ 四、公平锁(FairLock)的死锁陷阱
队列状态不一致
- 场景:
- 客户端A获取锁后发生长时间GC暂停
- 看门狗续期失败,锁过期被删除
- 客户端B获取锁并修改队列
- 结果:
客户端A恢复后仍认为自己是持有者,强制解锁破坏队列,后续线程陷入永久阻塞
🛡️ 五、最佳实践
锁类型 | 死锁原因 | 解决方案 |
---|---|---|
基础锁 | 看门狗续期失败 | 设置合理超时时间(lock.tryLock(10, 30, TimeUnit.SECONDS) ),避免依赖看门狗[4] |
读写锁 | 读锁升级写锁 | 禁止锁升级,写锁需直接获取而非由读锁升级[1] |
红锁 | 时钟漂移 | 使用NTP同步集群时间,并采用单调递增时钟计算有效期[1] |
公平锁 | 队列状态异常 | 启用redisson-lock 的leaseTime 参数,避免依赖看门狗续期[3] |
通用方案 | 嵌套锁循环等待 | 全局统一锁获取顺序,或用MultiLock 原子获取多锁[1] |
💎 总结
- 死锁根源:
- 锁续期机制与业务执行状态割裂(如网络闪断导致看门狗失效)[3]
- 分布式环境时钟不一致(红锁场景)[1]
- 锁设计违背安全原则(如读写锁升级)[1]
- 规避铁律:
- 永远设置显式超时(即使启用看门狗)
- 避免在锁内调用外部服务(防止长阻塞)
- 使用
tryLock
而非lock
,设置等待超时[4]
生产环境推荐:
- 监控锁续期线程状态:
Redisson.EXPIRATION_RENEWAL_MAP
[3]- 定期扫描长期持有锁的客户端:
redis-cli --eval check_long_hold_lock.lua