Redisson 看门狗机制详解

Redisson 看门狗机制详解

注意:从语雀导出的文章格式有问题,请从语雀查看

在分布式系统中,分布式锁是协调多节点并发访问共享资源的核心组件。Redis 因其高性能和丰富的数据结构,成为实现分布式锁的热门选择。Redisson 作为 Redis 的 Java 客户端,不仅提供了简单易用的分布式锁 API,其内置的"看门狗" 机制** 更是解决了锁的自动续期难题,极大地提升了锁的可靠性和易用性。

一、 为什么需要看门狗?------ 从锁的释放讲起

要理解看门狗,首先要明白一个关键问题:锁应该在什么时候被释放?

一个最直接的答案是:在业务逻辑执行完毕后,由锁的持有者主动释放。这听起来很简单,但在分布式环境中,情况要复杂得多。

考虑以下场景:

  1. 正常情况:线程 A 获取锁 -> 执行业务逻辑 -> 释放锁。
  2. 异常情况 :线程 A 获取锁 -> 在执行业务逻辑时,所在服务器发生长时间 GC、网络抖动、或是服务实例意外宕机 -> 线程 A 被阻塞或"死亡",无法执行释放锁的代码。

在异常情况下,如果锁没有被释放,它就会一直存在于 Redis 中,导致其他所有线程都无法再获取到这个锁,这就是可怕的 "死锁" 问题。

为了避免死锁,我们通常会给锁设置一个 过期时间(TTL)。这样,即使锁的持有者崩溃,锁也会在 TTL 过后自动失效。

但是,设置 TTL 又引入了新的问题:

  • TTL 设置多久?
    • 如果设置得太短,业务逻辑还没执行完,锁就自动释放了。此时线程 B 可以获取到锁,导致数据不一致。
    • 如果设置得太长,万一持有锁的线程真的崩溃了,其他线程需要等待很长时间才能重新获取锁,系统可用性降低。

看门狗机制就是为了解决这个"TTL 设置难题"而生的。它的核心思想是:只要持有锁的线程还活着,并且业务还在执行,我就不断地延长锁的过期时间。

二、 看门狗机制的核心原理

Redisson 的看门狗机制可以概括为 "只要锁未被显式释放,且持有锁的客户端依然存活,就自动为锁续命"

其工作流程如下图所示:

具体来说,其工作流程如下:

  1. 获取锁与看门狗启动
    • 当使用 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">lock()</font> 方法(不传入 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">leaseTime</font> 参数)获取锁时,看门狗机制会被激活。
    • 锁在 Redis 中的默认超时时间是 30 秒
    • 在成功获取锁之后,Redisson 会向一个后台的守护线程池中提交一个定时任务。
  2. 定时续期
    • 这个定时任务首次会在 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">锁默认超时时间 / 3</font> 后执行,也就是 10 秒 后。
    • 任务执行时,它会检查当前客户端是否还持有这个锁(通过比较 Redis 中锁的值和客户端的唯一 ID)。
    • 如果仍然持有,它就通过执行 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">PEXPIRE</font> 命令,将锁的过期时间重新设置为 30 秒
    • 这个"检查 -> 续期"的循环会每隔 10 秒执行一次,直到锁被释放或客户端宕机。
  3. 释放或终止
    • 主动释放 :当业务代码执行完毕,调用 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">unlock()</font> 方法时,在释放锁的同时,会取消这个对应的定时任务。
    • 被动终止:如果持有锁的 JVM 进程崩溃,那么看门狗线程也会随之停止,续期任务自然终止。锁在最后一次续期后的 30 秒会自动过期,避免了永久死锁。

关键点总结:

  • 触发条件 :使用无参 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">lock()</font><font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">tryLock()</font> 方法,且不指定 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">leaseTime</font>
  • 锁超时时间:默认 30 秒。
  • 续期间隔 :默认 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">超时时间 / 3</font>,即 10 秒。
  • 续期操作:将锁的 TTL 重置为初始的超时时间(30秒)。

三、 源码层面的简单剖析

让我们深入到 Redisson 源码中,看看看门狗是如何实现的(以 ## **edisson 3.x 版本为例)。

锁获取与看门狗启动

<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">RedissonLock#lock()</font> 方法中,最终会调用 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">tryAcquireAsync</font> 方法。在异步获取锁成功后,会调用 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">scheduleExpirationRenewal</font> 方法。

  1. java
java 复制代码
// RedissonLock.class
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    // ... 省略前期代码 ...
    long threadId = Thread.currentThread().getId();
    Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
    // ... 省略后续代码 ...
}

private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    // 调用内部异步方法
    return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Long> ttlRemainingFuture;
    if (leaseTime != -1) {
        // 如果指定了leaseTime,则不会启动看门狗
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        // 使用默认leaseTime(-1),会启动看门狗
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }

    // 获取锁成功后,执行后续操作
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }
        // lock acquired
        if (ttlRemaining == null) {
            // 这里是关键:安排过期时间的续期
            if (leaseTime != -1) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                // 启动看门狗任务!
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

看门狗任务的核心
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">scheduleExpirationRenewal</font> 方法会创建一个 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">ExpirationEntry</font> 并启动一个定时任务。这个任务的核心是 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">renewExpiration</font> 方法。

  1. java
java 复制代码
// RedissonBaseLock.class
protected void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }

    // 创建一个延时任务
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }

            // 核心:异步执行续期命令
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getRawName() + " expiration", e);
                    // 如果出错,不再续期
                    return;
                }

                if (res) {
                    // 续期成功,则递归调用自己,安排下一次续期
                    renewExpiration();
                }
                // 如果res为false,表示锁已经不存在(已被释放),则任务结束。
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 延时时间为 internalLockLeaseTime / 3

    ee.setTimeout(task);
}

Lua 脚本续期
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">renewExpirationAsync</font> 方法会执行一段 Lua 脚本,这是保证原子性的关键。

  1. lua
java 复制代码
-- 位于 RedissonLock.renewExpirationAsync 方法中
-- 判断 Redis 中锁的值是否仍是当前客户端的标识
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
    -- 如果是,则执行 pexpire 命令,重置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]); 
return 1; 
end; 
return 0;
复制代码
- `<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">KEYS[1]</font>`<font style="color:rgb(15, 17, 21);">:锁的 Key。</font>
- `<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">ARGV[1]</font>`<font style="color:rgb(15, 17, 21);">:新的过期时间(默认 30000 毫秒)。</font>
- `<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">ARGV[2]</font>`<font style="color:rgb(15, 17, 21);">:客户端唯一标识(</font>`<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">getLockName(threadId)</font>`<font style="color:rgb(15, 17, 21);">)。</font>

从源码可以看出,整个流程通过异步回调和非阻塞的延时任务巧妙地串联起来,既保证了性能,又实现了可靠的自动续期。

四、 如何使用与配置

1. 启用看门狗(默认行为)

java

java 复制代码
RLock lock = redisson.getLock("myLock");
// 以下两种方式都会启用看门狗
lock.lock();
// 或
lock.tryLock();

2. 禁用看门狗(手动指定租约时间)

如果你为锁指定了 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">leaseTime</font> 参数,看门狗机制将不会被启用。锁会在指定的 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">leaseTime</font> 后自动过期,无论业务是否执行完毕。

java

java 复制代码
// 获取锁后,最多持有10秒,10秒后自动释放,不会续期
lock.lock(10, TimeUnit.SECONDS);

// 尝试获取锁,等待100秒,获取成功后持有5秒
boolean isLocked = lock.tryLock(100, 5, TimeUnit.SECONDS);
if (isLocked) {
    try {
        // ... 业务逻辑,必须在5秒内完成 ...
    } finally {
        lock.unlock();
    }
}

3. 关键配置参数

你可以在 Redisson 的配置文件中修改看门狗的默认参数:

java

java 复制代码
Config config = new Config();
config.setLockWatchdogTimeout(30 * 1000); // 单位毫秒,默认就是30000ms
RedissonClient redisson = Redisson.create(config);
  • <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">lockWatchdogTimeout</font>:这个参数控制了看门狗机制中锁的默认超时时间。续期间隔是该时间的 1/3。

五、 最佳实践与注意事项

一定要在 finally 块中解锁

这是最基本也是最重要的一点。确保无论业务逻辑正常执行还是抛出异常,锁都能被释放,并取消看门狗任务。

  1. java
plain 复制代码
lock.lock();
try {
    // ... 业务逻辑 ...
} finally {
    lock.unlock();
}
  1. 评估业务执行时间
    看门狗保证了长任务下的锁安全,但并不意味着你可以无限制地执行长任务。长时间持有锁会严重影响系统吞吐量和响应速度。请尽量优化业务逻辑,缩短锁的持有时间。
  2. 明确使用场景
    • 使用看门狗:适用于无法准确预估业务执行时间的场景,希望最大程度避免因锁自动过期而导致的数据不一致。
    • 指定 leaseTime:适用于能够明确知道业务最大执行时间的场景,可以作为一层额外的保护,防止看门狗因未知原因(如系统时钟问题)失效而导致锁无法释放。
  3. 避免混用
    不要在同一个锁上,有时用看门狗,有时又指定 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">leaseTime</font>,这会造成行为的不一致,增加维护的复杂性。
  4. 理解它不是"银弹"
    看门狗解决了持有者存活时的续期问题,但无法解决所有分布式锁的难题,例如:
    • Redis 脑裂:在主从切换时,可能会导致锁在旧主和新主上同时被持有。
    • 时钟跳跃:如果服务器发生剧烈的时钟跳跃,可能会影响 TTL 的判断。

对于可靠性要求极高的场景,可以考虑使用 Redisson 的 RedLock(红锁),但它也带来了更高的复杂性和性能开销。

六、 总结

Redisson 的看门狗机制是一个精妙而实用的设计,它将开发者从手动设置锁超时时间的困境中解放出来。通过后台守护线程的定期续期,它实现了 "业务不停,锁不释放" 的智能效果,极大地增强了分布式锁在不可靠网络和复杂业务环境下的健壮性。

理解其原理、正确使用并规避其陷阱,是你在分布式系统中用好 Redisson 分布式锁的关键一步。

相关推荐
那我掉的头发算什么2 小时前
【javaEE】多线程——线程安全进阶☆☆☆
java·jvm·安全·java-ee·intellij-idea
悟空CRM服务2 小时前
我用一条命令部署了完整CRM系统!
java·人工智能·开源·开源软件
组合缺一2 小时前
Solon AI 开发学习 - 1导引
java·人工智能·学习·ai·openai·solon
百***49002 小时前
基于SpringBoot和PostGIS的各省与地级市空间距离分析
java·spring boot·spring
Unstoppable222 小时前
八股训练营第 21 天 | Redis的数据类型有哪些?Redis是单线程的还是多线程的,为什么?说一说Redis持久化机制有哪些?
数据库·redis·缓存·八股
无心水2 小时前
【分布式利器:Redis】Redis基本原理详解:数据模型、核心特性与实战要点
数据库·redis·缓存·数据模型·i/o多路复用·redis高并发·redis基本原理
大头an2 小时前
Redis内存碎片深度解析:从动态整理到核心运维实践
数据库·redis
电摇小人2 小时前
科学备赛今年NOIP!!
java·开发语言
未若君雅裁2 小时前
LeetCode 18 - 四数之和 详解笔记
java·数据结构·笔记·算法·leetcode