实战篇 20. 分布式锁 - Redisson 看门狗与重试机制原理

5 阅读4分钟

📚 实战篇 20. 分布式锁 - Redisson 看门狗与重试机制原理

一、 整体执行入口:tryLock

当我们调用 lock.tryLock(waitTime, leaseTime, TimeUnit) 时,Redisson 底层会进入 tryAcquire 方法,并最终异步执行加锁的 Lua 脚本(也就是图中最顶部的 tryAcquireAsync)。

这个 Lua 脚本执行完后,只会产生两种结果,这也决定了流程图接下来的走向:

  1. 返回 null 代表成功抢到了锁(走流程图左侧分支)。
  2. 返回 pttl(剩余有效期): 代表锁被别人占了,抢锁失败(走流程图右侧分支)。

二、 右侧分支:抢锁失败后的“重试机制”

我们手写的 V3 版本如果抢不到锁,直接就 return false 报错了。来看看 Redisson 是怎么优雅重试的:

  1. 计算剩余等待时间: 首先判断你传入的 waitTime(最大等待时间)还有没有剩余。如果已经超时了,直接返回 false 宣告失败。

  2. 订阅释放消息 (subscribe): 如果还有时间,它不会立刻去死循环疯狂查数据库(那样会把 Redis CPU 打满)。而是利用 Redis 的 Pub/Sub(发布订阅)机制,订阅这把锁的专属频道。

  3. 阻塞等待 (await): 线程会在这里进入阻塞状态,静静等待。等什么呢?等那个拿到锁的线程执行完业务,调用 unlock() 时发布一条“我释放锁啦”的消息。

  4. 被唤醒并死循环重试 (while(true)):

    • 一旦收到了锁释放的通知(或者等待超时了),线程就会被唤醒。
    • 唤醒后,它会进入一个 while(true) 的死循环,再次计算剩余等待时间。
    • 只要时间还够,就再次调用 Lua 脚本去尝试抢锁。
    • 抢到了就成功返回 true,抢不到就继续订阅、阻塞、等待唤醒...周而复始。

💡 核心优势: 这种基于“订阅 + 阻塞唤醒”的机制,极大地避免了无效的 CPU 空转,是高并发下极其优雅的重试方案。


三、 左侧分支:抢锁成功后的“看门狗机制” (核心考点)

如果你的业务非常复杂,执行了 40 秒,而锁的默认 TTL 只有 30 秒,锁提前释放导致并发安全被破坏怎么办?看门狗就是为了解决这个“超时命题”而生的。

结合流程图左侧,看门狗的运作逻辑如下:

  1. 触发条件: 当 Lua 脚本返回 null(加锁成功),且你没有手动设置 leaseTime(或者 leaseTime = -1)时,Redisson 就会触发看门狗机制。

  2. 开启定时任务 (scheduleExpirationRenewal): Redisson 会在后台启动一个名为 Timeout 的定时任务(这就是那只“看门狗”)。

  3. 自动续期规则:

    • 默认情况下,看门狗的锁超时时间(internalLockLeaseTime)是 30 秒
    • 这个定时任务每隔 internalLockLeaseTime / 3(也就是 10 秒)就会跑一次。
    • 每次跑的时候,它都会异步执行一段 Lua 脚本(renewExpirationAsync),核心逻辑就是:重新把这把锁的 TTL 刷新回 30 秒!
  4. 生生不息: 只要你的业务还在执行,并且服务器没宕机,这只看门狗就会每隔 10 秒给你续命一次。这把锁永远都不会因为超时而被自动释放!


四、 闭环:业务执行完毕后的清理

当你的业务逻辑终于执行完了,主动调用 lock.unlock() 时,Redisson 会做两件非常重要的事情:

  1. 取消看门狗 (cancelExpirationRenewal): 既然业务都执行完了,锁都要释放了,必须把后台那个每 10 秒跑一次的定时任务给停掉,否则这把锁就真成了删不掉的死锁了。
  2. 发布释放消息 (publish): 执行解锁的 Lua 脚本,真正删除 Hash 结构中的数据,并立刻向专属频道发布一条消息,唤醒那些在流程图右侧苦苦阻塞等待的兄弟线程,告诉它们:“我完事了,你们上!”

五、 学习总结

结合你上传的这张图,Redisson 的底层逻辑其实就是一套严密的组合拳:

  • Hash 结构 解决重入问题。
  • 发布订阅 + 死循环 解决重试和等待问题。
  • 后台定时任务 (WatchDog) 解决业务执行超时问题。