📚 实战篇 20. 分布式锁 - Redisson 看门狗与重试机制原理
一、 整体执行入口:tryLock
当我们调用 lock.tryLock(waitTime, leaseTime, TimeUnit) 时,Redisson 底层会进入 tryAcquire 方法,并最终异步执行加锁的 Lua 脚本(也就是图中最顶部的 tryAcquireAsync)。
这个 Lua 脚本执行完后,只会产生两种结果,这也决定了流程图接下来的走向:
- 返回
null: 代表成功抢到了锁(走流程图左侧分支)。 - 返回
pttl(剩余有效期): 代表锁被别人占了,抢锁失败(走流程图右侧分支)。
二、 右侧分支:抢锁失败后的“重试机制”
我们手写的 V3 版本如果抢不到锁,直接就 return false 报错了。来看看 Redisson 是怎么优雅重试的:
-
计算剩余等待时间: 首先判断你传入的
waitTime(最大等待时间)还有没有剩余。如果已经超时了,直接返回false宣告失败。 -
订阅释放消息 (
subscribe): 如果还有时间,它不会立刻去死循环疯狂查数据库(那样会把 Redis CPU 打满)。而是利用 Redis 的 Pub/Sub(发布订阅)机制,订阅这把锁的专属频道。 -
阻塞等待 (
await): 线程会在这里进入阻塞状态,静静等待。等什么呢?等那个拿到锁的线程执行完业务,调用unlock()时发布一条“我释放锁啦”的消息。 -
被唤醒并死循环重试 (
while(true)):- 一旦收到了锁释放的通知(或者等待超时了),线程就会被唤醒。
- 唤醒后,它会进入一个
while(true)的死循环,再次计算剩余等待时间。 - 只要时间还够,就再次调用 Lua 脚本去尝试抢锁。
- 抢到了就成功返回
true,抢不到就继续订阅、阻塞、等待唤醒...周而复始。
💡 核心优势: 这种基于“订阅 + 阻塞唤醒”的机制,极大地避免了无效的 CPU 空转,是高并发下极其优雅的重试方案。
三、 左侧分支:抢锁成功后的“看门狗机制” (核心考点)
如果你的业务非常复杂,执行了 40 秒,而锁的默认 TTL 只有 30 秒,锁提前释放导致并发安全被破坏怎么办?看门狗就是为了解决这个“超时命题”而生的。
结合流程图左侧,看门狗的运作逻辑如下:
-
触发条件: 当 Lua 脚本返回
null(加锁成功),且你没有手动设置leaseTime(或者leaseTime = -1)时,Redisson 就会触发看门狗机制。 -
开启定时任务 (
scheduleExpirationRenewal): Redisson 会在后台启动一个名为Timeout的定时任务(这就是那只“看门狗”)。 -
自动续期规则:
- 默认情况下,看门狗的锁超时时间(
internalLockLeaseTime)是 30 秒。 - 这个定时任务每隔
internalLockLeaseTime / 3(也就是 10 秒)就会跑一次。 - 每次跑的时候,它都会异步执行一段 Lua 脚本(
renewExpirationAsync),核心逻辑就是:重新把这把锁的 TTL 刷新回 30 秒!
- 默认情况下,看门狗的锁超时时间(
-
生生不息: 只要你的业务还在执行,并且服务器没宕机,这只看门狗就会每隔 10 秒给你续命一次。这把锁永远都不会因为超时而被自动释放!
四、 闭环:业务执行完毕后的清理
当你的业务逻辑终于执行完了,主动调用 lock.unlock() 时,Redisson 会做两件非常重要的事情:
- 取消看门狗 (
cancelExpirationRenewal): 既然业务都执行完了,锁都要释放了,必须把后台那个每 10 秒跑一次的定时任务给停掉,否则这把锁就真成了删不掉的死锁了。 - 发布释放消息 (
publish): 执行解锁的 Lua 脚本,真正删除 Hash 结构中的数据,并立刻向专属频道发布一条消息,唤醒那些在流程图右侧苦苦阻塞等待的兄弟线程,告诉它们:“我完事了,你们上!”
五、 学习总结
结合你上传的这张图,Redisson 的底层逻辑其实就是一套严密的组合拳:
- 用 Hash 结构 解决重入问题。
- 用 发布订阅 + 死循环 解决重试和等待问题。
- 用 后台定时任务 (WatchDog) 解决业务执行超时问题。