watchdog看门狗原理(史上最全):业务没完,锁过期咋整?Redis锁如何自动续期? 说说 看门狗原理 ?

watchdog看门狗原理(史上最全):业务没完,锁过期咋整?Redis锁如何自动续期? 说说 看门狗原理 ?

尼恩说在前面

在45岁老架构师尼恩的读者交流群(50+人)里,最近不少老铁拿到了阿里、滴滴、极兔、有赞、希音、百度、字节、网易、美团这些一线大厂的面试入场券,恭喜各位!

前两天就有个小伙伴面京东,被问到一个基础但杀伤力极强的面试题:

业务没执行完,锁已经过期了, 会发生什么?

Redis分布式锁如何自动续期?

Redisson锁自动续期机制怎么实现?

小伙伴 没有看过系统化的 答案,回答也不全面 ,so, 面试官不满意 , 面试挂了。

小伙伴找尼恩复盘, 求助尼恩。 这里尼恩给大家做一下 系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。

同时,也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典PDF》V176版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书

面试官问:Redis分布式锁如何自动续期?Redisson锁自动续期机制怎么实现?

首先,回顾什么是Redis 分布式锁?

大致是:

Redis 分布式锁 首先 指定一个 key ,这个key作为锁标记;

然后,指定一个 唯一的用户标识作为 value。

最后,通过设置 Key的过期时间,来防止死锁的情况。

这里就会产生一非常大问题:

假设锁的过期时间是30s,但是业务运行超过了50s,业务没执行完,锁已经过期了, 失去了锁的效果。

这时为了不让其他客户端拿到锁,需要给锁进行续期,那么,该如何续期?

所以如何实现 锁续期? 是Redis分布式锁的一个重要问题。

今天,我们就从基础使用到架构升华,层层拆解Redisson分布式锁自动续期的奥秘,既有底层源码的深度,也有架构设计的高度。

一、基础应用--利用Redisson实现锁的自动续期

首先是 直接用现成的 轮子。

我们直接 采用Redisson实现锁的自动续期。

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。基于Netty框架实现,早已成为生产环境中分布式锁的“首选工具”。

Redisson 最贴心的地方,就是将自动续期 封装成“黑盒”,在 Redission 叫做 看门狗机制,通过 这个机制,开发者无需关心底层实现,几行代码就能实现安全可靠的分布式锁,彻底告别手动续期的烦恼。

Redisson的具体使用步骤如下:

1、加入jar包的依赖

org.redisson

redisson

2、配置Redisson

public class RedissonManager {

private static Config config = new Config();

//声明redisso对象

private static Redisson redisson = null;

//实例化redisson

static{

config.useClusterServers()

// 集群状态扫描间隔时间,单位是毫秒

.setScanInterval(2000)

//cluster方式至少6个节点(3主3从,3主做sharding,3从用来保证主宕机后可以高可用)

.addNodeAddress("redis://127.0.0.1:6379" )

.addNodeAddress("redis://127.0.0.1:6380")

.addNodeAddress("redis://127.0.0.1:6381")

.addNodeAddress("redis://127.0.0.1:6382")

.addNodeAddress("redis://127.0.0.1:6383")

.addNodeAddress("redis://127.0.0.1:6384");

//得到redisson对象

redisson = (Redisson) Redisson.create(config);

}

//获取redisson对象的方法

public static Redisson getRedisson(){

return redisson;

}

}

3、锁的获取和释放

public class DistributedRedisLock {

//从配置类中获取redisson对象

private static Redisson redisson = RedissonManager.getRedisson();

private static final String LOCK_TITLE = "redisLock_";

//加锁

public static boolean acquire(String lockName){

//声明key对象

String key = LOCK_TITLE + lockName;

//获取锁对象

RLock mylock = redisson.getLock(key);

//加锁,启动看门狗, 无参lock(),启用看门狗

mylock.lock();// 自动续期,默认30秒超时,看门狗每隔10秒续期

return true;

}

//锁的释放

public static void release(String lockName){

//必须是和加锁时的同一个key

String key = LOCK_TITLE + lockName;

//获取所对象

RLock mylock = redisson.getLock(key);

//释放锁(解锁)

mylock.unlock();

}

}

4、锁的使用

public String discount() throws IOException{

String key = "lock001";

//加锁

DistributedRedisLock.acquire(key);

//执行具体业务逻辑

dosoming

//释放锁

DistributedRedisLock.release(key);

//返回结果

return soming;

}

5、配置看门狗超时时间

可以通过配置修改默认的锁超时时间(看门狗超时时间)。

Config config = new Config();

config.setLockWatchdogTimeout(30000L); // 默认30秒,单位毫秒

6、什么场景启用看门狗,什么场景不启用呢?

Redisson 中看门狗的启用规则非常明确:

当你调用 lock() 无参方法 时(如代码中 mylock.lock();),Redisson 会自动启用看门狗机制;

而调用 lock(long leaseTime, TimeUnit unit) 带过期时间的方法 时,看门狗会被禁用。

上面的代码中使用的是无参lock(),因此:

1、 初始会给 Redis 中的锁设置一个默认超时时间(默认 30 秒,可通过setLockWatchdogTimeout修改);

2、 看门狗线程会在后台每隔 超时时间/3(默认 10 秒)检查一次,如果业务逻辑还没执行完,就自动把锁的超时时间续期到 30 秒;

3、 直到你调用unlock()释放锁,看门狗才会停止。

需要 启用看门狗的场景,主要有哪些呢(推荐场景)?

首先,回顾一下看门狗本质。 看门狗本质 是 Redisson 的自动续期机制,解决的核心问题是:防止业务逻辑执行时间超过锁的过期时间,导致锁提前释放,引发分布式并发问题。

举个例子:

你的业务逻辑需要执行 40 秒,但锁的超时时间只有 30 秒;

如果没有看门狗,锁会在 30 秒后自动过期,其他线程会拿到锁,导致同一资源被多个线程操作;

有了看门狗,会每隔 10 秒把锁续期 30 秒,直到 40 秒后业务执行完、调用unlock(),锁才会释放。

适合用看门狗(无参lock() + 自动续期)的场景 大致有:

1、 业务执行时间不确定:比如处理订单、调用第三方接口、数据库复杂查询等,执行时间受网络 / 数据量影响,无法预估固定时长;

2、 希望锁随业务逻辑自动释放:不需要手动计算过期时间,Redisson 会保证 “业务没执行完,锁就不会过期;业务执行完,调用 unlock () 立即释放”;

3、 高并发、数据一致性要求高的场景:比如库存扣减、秒杀下单、资金交易等,必须确保同一资源同一时间只有一个线程处理。

示例(启用看门狗):

// 无参lock(),启用看门狗

RLock lock = redisson.getLock("order_lock_123");

lock.lock(); // 自动续期,默认30秒超时,看门狗每隔10秒续期

try {

// 执行不确定时长的业务:调用第三方支付接口

payService.callThirdPay();

} finally {

lock.unlock(); // 释放锁,看门狗停止

}

需要 不启用看门狗的场景(禁用场景) ,大致有哪些呢?

1、 业务执行时间固定且可控:比如简单的缓存更新、数据查询,能明确预估执行时间(如 10 秒),可以设置一个稍大的固定过期时间(如 15 秒);

2、 分布式锁作为 “临时占位”:比如分布式任务调度,只需要锁存在固定时长,超时自动释放即可。

这些场景 适合用带过期时间的lock(leaseTime, unit), 也就是 不启用看门狗。

示例(禁用看门狗):

// 带参lock(),禁用看门狗,锁固定15秒后过期

RLock lock = redisson.getLock("cache_lock_456");

lock.lock(15, TimeUnit.SECONDS); // 15秒后自动过期,不会续期

try {

// 执行固定时长的业务:更新缓存(预估5秒)

cacheService.updateCache();

} finally {

// 手动释放锁(即使没执行完,15秒后也会自动释放)

if (lock.isHeldByCurrentThread()) {

lock.unlock();

}

}

注意:超时时间配置,需要合理:

setLockWatchdogTimeout不要设置过小(如 1 秒),否则续期频率太高,增加 Redis 压力;

也不要设置过大(如 10 分钟),否则服务宕机后,锁需要很久才会释放。

二、Redisson分布式锁的实现原理和加锁机制

Redisson分布式锁的实现原理如下图:

加锁机制

如果 客户端 Redisson 面对的是一个redis cluster集群,他首先会根据hash算法, 选择一台 redis master 节点。

发送lua脚本到 redis master 节点上,lua 脚本如下:

"if (redis.call('exists',KEYS[1])==0) then "+ --看有没有锁

"redis.call('hset',KEYS[1],ARGV[2],1) ; "+ --无锁 加锁

"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+

"return nil; end ;" +

"if (redis.call('hexists',KEYS[1],ARGV[2]) ==1 ) then "+ --我加的锁

"redis.call('hincrby',KEYS[1],ARGV[2],1) ; "+ --重入锁

"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+

"return nil; end ;" +

"return redis.call('pttl',KEYS[1]) ;" --不能加锁,返回锁的时间

lua的作用:保证这段复杂业务逻辑执行的原子性。

KEYS[1]) : 加锁的key

ARGV[1] : key的生存时间,默认为30秒

ARGV[2] : 加锁的客户端ID (UUID.randomUUID()) + “:” + threadId)

注意 : Redisson 的 Lua 脚本执行 有一个优化机制 ,Redisson 不会每次都发送完整脚本 , Redisson 采用 EVALSHA 机制优化

首次执行:发送完整 Lua 脚本,Redis 缓存脚本并返回 SHA1

后续执行:只发送 SHA1 摘要,Redis 通过摘要查找缓存执行

Redisson 采用 EVALSHA 机制来优化脚本执行:

// 伪代码示意

String luaScript = "if (redis.call('exists',KEYS[1])==0) then ..."; // 完整脚本

String scriptSha1 = sha1(luaScript); // 计算脚本的SHA1摘要

// 实际执行时

try {

// 先尝试用 SHA1 执行

return redis.evalsha(scriptSha1, keys, args);

} catch (NoScriptException e) {

// 如果 Redis 中没有缓存此脚本

// 则完整发送脚本并缓存

redis.eval(luaScript, keys, args);

// 后续调用都使用 SHA1

}

客户端本地缓存:Redisson 在启动时会预计算所有内置 Lua 脚本的 SHA1 值

Redis 服务端缓存:脚本在 Redis 中被执行一次后会被永久缓存(直到 Redis 重启)

集群环境:每个 Master 节点独立缓存脚本,客户端会与每个节点建立脚本缓存

三、看门狗的工作原理和流程

当客户端调用无参lock()不指定过期时间时,Redisson会自动启动看门狗。

Redisson 看门狗 工作流程可概括为“启动-续期-停止”三步,形成周期性续期的闭环:

第一步:启动看门狗

加锁成功后,Redisson会判断:如果未指定leaseTime(过期时间),则默认设置过期时间为30秒(可通过配置修改),同时启动一个后台定时线程(看门狗),并记录当前客户端持有锁的状态。

注意,很多小伙伴不懂netty, 所以上面用一个 后台定时线程(看门狗 线程) 来表述。

其实 这个后台定时线程(看门狗)不是一个专门的线程,而是Netty的 事件循环(EventLoop)实现,任务执行不阻塞主线程,符合 Redisson 异步通信的设计理念。

关于这个线程,稍后一点给大家介绍, 所以,要真正搞懂java,还是要懂netty,具体请参考尼恩的《java高并发核心编程 卷1》

第二步:周期性续期

看门狗的续期时机非常巧妙——并非等到锁快过期才续期,而是在锁过期时间的1/3处触发续期。

例如:默认过期时间30秒,看门狗任务 会每隔10秒(30/3)检查一次锁的状态:

如果客户端仍持有锁(业务未执行完),则发送Lua脚本到Redis,将锁的过期时间重置为30秒;

续期成功后,会再创建一个看门狗任务, 再次定时(10秒后)检查,形成循环;

如果续期失败(如Redis宕机、锁已被释放),则停止续期。

第三步:停止看门狗

当客户端调用unlock()方法释放锁,或客户端宕机时,看门狗会自动停止:

正常释放锁:调用unlock()时,Redisson会清理看门狗的定时任务,同时删除Redis中的锁Key(或递减重入次数);

客户端宕机:“看门狗定时任务” 随客户端进程终止,不再续期,Redis中的锁Key会在过期时间到后自动删除,避免死锁。

四、Redisson自动续期的源码分析

要毒打面试官, 光懂原理不够,还需要深入核心源码,看清关键类、关键方法的逻辑,以及背后隐藏的设计模式。

我们选取最核心的2个类、3个方法,拆解续期的底层实现,同时点明其用到的设计模式。

1、主类RedissonLock

关键方法1:lock()——启动看门狗的触发点

public class RedissonLock extends RedissonExpirable implements RLock {

// 默认看门狗超时时间(30秒)

private long lockWatchdogTimeout = 30000L;

// 无参数lock():自动启动看门狗

@Override

public void lock() {

lock(-1, null, false);

}

// 核心重载方法:leaseTime=-1时启动看门狗

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {

long threadId = Thread.currentThread().getId();

// 尝试获取锁,返回锁的剩余过期时间

Long ttl = tryAcquire(-1, leaseTime, unit, threadId);

// ttl==null表示加锁成功,后续逻辑省略...

}

}

2、看门狗启动入口

看门狗任务调度

// RedissonLock.java

private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {

return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));

}

private RFuture tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {

RFuture ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);

ttlRemainingFuture.onComplete((ttlRemaining, e) -> {

if (e != null) {

return;

}

// 锁获取成功

if (ttlRemaining == null) {

// 看门狗启动的关键:当 leaseTime = -1 时

if (leaseTime != -1) {

internalLockLeaseTime = unit.toMillis(leaseTime);

} else {

// 启动看门狗续期

scheduleExpirationRenewal(threadId);

}

}

});

return ttlRemainingFuture;

}

关键方法2:scheduleExpirationRenewal()——看门狗启动核心

// 启动看门狗续期(threadId:当前持有锁的线程ID)

protected void scheduleExpirationRenewal(long threadId) {

// 1. 创建续期状态对象,记录线程ID和重入次数

ExpirationEntry entry = new ExpirationEntry();

// 2. 存入全局Map,保证线程安全(ConcurrentHashMap)

ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);

if (oldEntry != null) {

// 3. 同一线程重入锁,累加线程计数

oldEntry.addThreadId(threadId);

} else {

// 4. 首次加锁,启动续期任务

entry.addThreadId(threadId);

renewExpiration(); // 真正执行续期的方法

}

}

3、看门狗周期性续期核心实现

关键方法3:renewExpiration()——周期性续期的核心实现

private void renewExpiration() {

ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());

if (ee == null) {

return; // 续期状态不存在,直接返回

}

// 1. 创建定时任务(Netty的TimerTask,非JDK原生Timer)

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;

}

// 2. 获取持有锁的线程ID

Long threadId = ent.getFirstThreadId();

if (threadId == null) {

return;

}

// 3. 异步续期:发送Lua脚本到Redis

RFuture future = renewExpirationAsync(threadId);

future.whenComplete((res, e) -> {

if (e != null) {

// 4. 续期异常,记录日志,停止续期

log.error("Can't update lock expiration", e);

return;

}

if (res) {

// 5. 续期成功,递归调用,实现循环续期

renewExpiration();

} else {

// 6. 续期失败,取消续期

cancelExpirationRenewal(threadId);

}

});

}

}, lockWatchdogTimeout / 3, TimeUnit.MILLISECONDS); // 1/3超时时间触发

// 7. 记录定时任务,方便后续取消

ee.setTimeout(task);

}

异步续期方法

// RedissonLock.java

protected RFuture renewExpirationAsync(long threadId) {

return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,

// Lua 脚本:如果锁存在且持有者是当前线程,则续期

"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +

"redis.call('pexpire', KEYS[1], ARGV[1]); " +

"return 1; " +

"end; " +

"return 0;",

Collections.singletonList(getName()),

internalLockLeaseTime, getLockName(threadId));

}

4、补充:Netty TimerTask 核心介绍

Redisson 中看门狗的续期定时任务基于Netty 的TimerTask(全类名:io.netty.util.TimerTask)实现,而非 JDK 原生java.util.Timer,这是 Netty 提供的高性能定时任务组件,核心由HashedWheelTimer(哈希轮定时器)驱动,是高性能异步定时任务的首选方案。

Netty TimerTask 核心特性

特性

说明

对比 JDK Timer 的优势

异步非阻塞

基于 Netty 的事件循环(EventLoop)实现,任务执行不阻塞主线程,符合 Redisson 异步通信的设计理念

JDK Timer 是单线程阻塞执行,一个任务耗时会阻塞其他定时任务

高性能

底层用HashedWheelTimer(哈希轮算法),时间复杂度 O (1),支持海量定时任务高效调度

JDK Timer 基于优先级队列,任务越多调度效率越低(O (logn))

任务取消

支持通过Timeout.cancel()主动取消未执行的定时任务,且取消操作轻量

JDK Timer 取消任务需遍历队列,效率低,且不支持 “未触发任务” 的精准取消

异常隔离

单个TimerTask执行抛出异常,不会导致整个定时器线程终止,仅记录日志

JDK Timer 中一个任务抛未捕获异常,整个 Timer 线程会挂掉,所有后续任务失效

毫秒级精度

支持毫秒级的定时触发,满足看门狗 “1/3 超时时间续期” 的精准性要求

JDK Timer 默认毫秒级,但受单线程阻塞影响,实际精度低

Redisson 中使用 Netty TimerTask 的核心逻辑

结合看门狗的renewExpiration()方法,Netty TimerTask 的使用流程如下:

1、创建定时器:

Redisson 通过 commandExecutor.getConnectionManager().newTimeout()获取 Netty 的 Timeout 对象(本质是 TimerTask的包装);

触发时间:lockWatchdogTimeout / 3(默认 10 秒);

任务载体:自定义TimerTask实现类,重写run()方法封装续期逻辑。

2、 执行续期任务:

定时时间到后,Netty 的 EventLoop 线程异步执行run()方法,不会阻塞 Redisson 的业务线程;

任务内完成 “校验续期状态→异步发送 Lua 脚本续期→处理续期结果”。

3、循环 / 终止任务:

续期成功:递归调用renewExpiration(),重新创建新的TimerTask(10 秒后触发),形成循环;

续期失败 / 解锁:调用Timeout.cancel()取消当前未执行的TimerTask,并清理本地ExpirationEntry中的Timeout引用。

为什么看门狗 用了 Netty TimerTask?

Redisson 作为高性能 Redis 客户端,核心通信基于 Netty 实现,选择 Netty TimerTask 而非 JDK Timer 的核心原因:

1、 技术栈统一:Redisson 的网络 IO、异步回调均基于 Netty,复用 Netty 的 EventLoop 线程池,避免额外创建线程池带来的资源开销;

2、 高可用要求:看门狗是分布式锁的核心保障,若用 JDK Timer,一旦某个续期任务抛异常,整个 Timer 线程挂掉,所有锁的续期都会失效,引发锁提前释放;

3、 高并发适配:在分布式场景下,一个 Redisson 客户端可能同时持有多个分布式锁,需同时运行多个看门狗续期任务,Netty HashedWheelTimer能高效处理海量定时任务,而 JDK Timer 会因单线程瓶颈导致续期延迟。

4、 Netty TimerTask是基于HashedWheelTimer的高性能异步定时任务组件,核心优势是异步非阻塞、异常隔离、精准取消;

5、 Redisson 看门狗用它实现续期任务,既保证了 “1/3 超时时间精准续期”,又适配了高并发、高可用的分布式锁场景;

6、 相较于 JDK Timer,Netty TimerTask 从根本上避免了 “单线程阻塞”“异常导致定时器崩溃” 等问题,是分布式锁续期的最优选择。

5、关键数据结构

ConcurrentHashMap(EXPIRATION_RENEWAL_MAP):全局存储所有正在续期的锁状态,保证线程安全,避免续期任务泄漏。

ExpirationEntry:管理续期状态,记录持有锁的线程ID和重入次数,避免多线程混乱;

全局续期映射表

// RedissonLock.java

// 存储所有正在续期的锁

protected static final ConcurrentMap EXPIRATION_RENEWAL_MAP = new ConcurrentHashMap<>();

Key 的设计:锁的唯一标识

Key 是分布式锁的完整名称(对应 Redis 中的锁 Key),比如 前代码中的redisLock_lock001,特点:

全局唯一:一个锁名称对应一个ExpirationEntry,不会重复;

与 Redis 中的锁 Key 完全一致:保证本地续期状态和 Redis 中的锁一一对应;

字符串类型:简单易匹配,符合 Redis Key 的命名规范。

Value 的设计:ExpirationEntry(续期状态载体)

Value 是自定义的ExpirationEntry对象,核心存储两类信息:

字段 / 方法

数据类型

核心作用

threadIds

Map

记录当前持有该锁的线程 ID和重入次数(Long = 线程 ID,Integer = 重入次数)

timeout

Netty Timeout

关联该锁的看门狗定时任务(TimerTask),用于后续取消续期

addThreadId()

方法

锁重入时,增加对应线程的重入次数

removeThreadId()

方法

释放锁时,递减重入次数(次数为 0 则移除线程 ID)

hasNoThreads()

方法

判断是否还有线程持有该锁(为空则可清理续期状态)

getFirstThreadId()

方法

获取第一个持有该锁的线程 ID(续期时校验合法性)

ExpirationEntry 的源码如下:

// RedissonLock.java

public static class ExpirationEntry {

// 记录持有锁的线程ID列表(支持可重入锁)

private final Map threadIds = new LinkedHashMap<>();

private volatile Timeout timeout;

public void addThreadId(long threadId) {

Integer counter = threadIds.get(threadId);

if (counter == null) {

counter = 1;

} else {

counter++;

}

threadIds.put(threadId, counter);

}

public boolean removeThreadId(long threadId) {

Integer counter = threadIds.get(threadId);

if (counter == null) {

return false;

}

counter--;

if (counter > 0) {

threadIds.put(threadId, counter);

} else {

threadIds.remove(threadId);

}

return true;

}

public boolean hasNoThreads() {

return threadIds.isEmpty();

}

public Long getFirstThreadId() {

if (threadIds.isEmpty()) {

return null;

}

return threadIds.keySet().iterator().next();

}

}

总结:

看门狗机制的正常运行,依赖Redisson内部多个组件的协作,具体如下:

RedissonLock:分布式锁的核心类,封装了加锁、释放锁、看门狗启动的入口;

ExpirationEntry:管理续期状态,记录持有锁的线程ID和重入次数,避免多线程混乱;

TimerTask:定时任务,负责周期性触发续期动作,由Netty的事件循环机制驱动;

ConcurrentHashMap(EXPIRATION_RENEWAL_MAP):全局存储所有正在续期的锁状态,保证线程安全,避免续期任务泄漏。

五、设计模式与思想:封装设计、观察者模式、单例模式、“防御式设计”

​ Redisson续期机制的源码中,隐藏了多种经典设计模式,正是这些设计模式让其代码具有高可读性、可扩展性和可靠性:

(1)封装设计

封装是面向对象编程(OOP)的四大核心特性(封装、继承、多态、抽象)之一,也是所有设计模式的基础,Redisson 源码中对 “封装” 的运用本质是面向对象思想的体现 。

将复杂的续期逻辑(看门狗启动、定时任务、Lua脚本调用)封装在RedissonLock类中,对外暴露简单的lock()、unlock()方法,开发者无需关心底层实现,符合“高内聚、低耦合”的架构原则。

用Lua脚本封装原子性操作,避免并发漏洞;

用看门狗线程封装续期逻辑,开发者无需手动启动线程;

用ExpirationEntry和全局Map封装续期状态,避免线程安全问题。

(2)观察者模式

定时任务的回调逻辑(future.whenComplete())采用了观察者模式:当异步续期任务完成(成功/失败)时,会自动触发回调方法,执行后续的续期或取消操作,无需主动轮询,提升性能。

(3)单例模式

Redisson客户端(RedissonClient)采用单例模式创建,确保整个应用中只有一个看门狗线程池,避免多实例导致的线程泄漏和资源浪费。

(4)“防御式设计”:杜绝死锁,提升可靠性

分布式锁的核心风险是死锁,Redisson的续期机制通过多重防御,从根本上杜绝了死锁的可能:

锁过期时间:即使看门狗失效,锁也会在过期时间后自动删除,避免死锁;

1/3续期时机:提前续期,避免因网络延迟、Redis卡顿导致续期不及时;

状态校验:续期前校验锁的持有者,避免为其他客户端的锁续期;

资源清理:锁释放时,自动清理看门狗任务和全局状态,避免内存泄漏。

六、Redisson续期 vs 其他续期方案对比

1、Redisson看门狗 vs 手动续期(守护线程)

对比维度

Redisson看门狗

手动续期(守护线程)

易用性

高:自动启动,无需手动编码

低:需手动管理线程、续期时机、异常处理

可靠性

高:多重校验,避免续期异常

低:易出现线程泄漏、续期不及时

性能

中:依赖Netty线程池,性能损耗低

低:手动线程管理,易出现性能瓶颈

灵活性

中:续期逻辑固定,可配置参数有限

高:可自定义续期时机、逻辑

2、Redisson看门狗 vs Redis原生过期回调(Keyspace Notifications)

另一种续期方案是利用Redis的键空间通知,监听锁Key的过期事件,在过期前触发续期。

如果不用 Redisson 的看门狗(Watchdog),而改用 Redis 原生的键空间通知(Keyspace Notifications)来实现续期,其核心思路是:监听特定 Key 的过期事件,在锁即将过期时,由监听者触发续期指令。

要实现 基于 Redis 键空间通知的分布式锁续期方案 ,大致步骤如下:

1、开启 Redis 键空间通知

默认情况下,Redis 禁用了键空间通知功能,因为它会消耗一定的 CPU 性能。你需要修改 redis.conf 或使用 CONFIG SET 指令。

# K: 键空间事件

# E: 键事件通知

# x: 过期事件(每当一个键过期时产生)

CONFIG SET notify-keyspace-events KEx

2、 逻辑实现步骤

步骤 A:加锁

客户端在 Redis 中设置一个带 TTL(过期时间)的锁,并确保锁的值包含客户端标识(UUID)。

步骤 B:订阅过期事件

客户端启动一个专门的监听进程或线程,订阅过期事件频道:

SUBSCRIBE __keyevent@0__:expired

步骤 C:执行续期(回调处理)

当监听到 expired 事件时,触发回调函数:

// 伪代码逻辑

redisSubscriber.on('message', (channel, expiredKey) => {

if (expiredKey === MY_LOCK_KEY) {

// 注意:此时锁可能已经完全消失了!

// 方案改进:通常需要监听 "即将过期" 或配合备用逻辑

// 真正的“续期”往往需要监听一个比主锁更早过期的“影子Key”

}

});

3、 “影子 Key” 技巧

由于 expired 事件是在 Key 已经删除 后才触发的,此时再续期已经晚了(锁已经被释放)。

为了实现续期,通常需要:

(1)、 主锁 Key:lock:order_1(TTL 30s)。

(2)、 影子 Key:shadow:lock:order_1(TTL 20s)。

(3)、 逻辑:监听 shadow Key 的过期。当 shadow 过期时,回调函数去检查主锁是否还在,如果在且业务未完,则重设两者 TTL。

4、 方案对比分析

特性

Redisson 看门狗 (主动续期)

键空间通知 (被动回调)

触发机制

客户端定时任务 (HashedWheelTimer)

Redis 服务端异步推送

实时性

极高,由客户端掌控

存在延迟,Redis 不保证消息能实时送达

可靠性

只要客户端不宕机,100% 续期

低。Pub/Sub 是“发后即忘”,丢包或监听者短暂掉线会导致锁直接失效

压力分布

分散在各个客户端

压力集中在 Redis 服务端(需推送大量通知)

适用场景

生产级分布式锁

简单的异步解耦、轻量级统计

总结:利用键空间通知做续期在理论上可行,但在高并发或网络抖动环境下极其危险。Redisson 采用客户端定时器(看门狗)而非通知机制,正是为了规避 Pub/Sub 的不可靠性。

Redis原生过期回调(Keyspace Notifications) 这种方案的局限性 :

可靠性低:键空间通知是“订阅-发布”模式,存在消息丢失风险;

性能损耗高:大量锁Key的过期通知会占用Redis资源;

实现复杂:需手动监听通知、校验锁状态、处理并发续期。

相比之下,Redisson看门狗的“主动续期”模式,可靠性更高、性能更优,更适合生产环境。

Redisson 分布式锁自动续期「高维暴击」回答思路

核心逻辑:从问题本质切入 → 基础使用层 → 原理层 → 源码层 → 设计层 → 方案对比层,层层递进,既解答 “是什么 / 怎么做”,又体现 “为什么这么做 / 比其他方案好在哪”,形成从 “使用” 到 “架构设计” 的完整闭环,超出面试官常规提问预期。

1、先破题:直击分布式锁续期的核心痛点(建立认知高度)

先回应 “Redis 分布式锁为什么需要续期”,锚定问题本质:

“Redis 分布式锁的核心是通过Key+唯一Value+过期时间实现分布式互斥,但过期时间是一把双刃剑 —— 设短了会导致业务未完成锁就失效,设长了会导致服务宕机后锁长期占用。而 Redisson 的看门狗机制,本质是客户端主动式的智能续期方案,完美解决了 “过期时间不可预估” 的核心矛盾。”

2、基础层:看门狗的使用规则(落地性回答)

用简洁语言讲清 “怎么用、什么时候用”,体现工程实践能力:

1、 启用规则:无参lock()自动启用看门狗,带过期时间lock(leaseTime, unit)禁用;

2、 核心效果:默认 30 秒过期,每 10 秒(过期时间 1/3)自动续期,业务完成调用unlock()后停止;

3、 场景选择:

启用:业务时长不确定(如调用第三方接口、复杂 DB 操作)、高一致性场景(秒杀 / 库存扣减);

禁用:业务时长固定(如简单缓存更新)、允许超时自动释放的场景(分布式任务调度)。

3、原理层:看门狗的核心流程(讲清 “怎么做”)

用 “三步闭环” 拆解,逻辑清晰且符合源码逻辑:

1、 启动:无参lock()加锁成功后,初始化ExpirationEntry续期状态并存入全局ConcurrentHashMap,调用scheduleExpirationRenewal()启动续期;

2、 续期:基于 Netty 的HashedWheelTimer定时任务(非 JDK Timer),每 1/3 过期时间触发,异步发送 Lua 脚本校验锁归属(当前线程持有),校验通过则重置锁过期时间,续期成功后递归创建新定时任务,形成循环;

3、 停止:正常解锁时清理ConcurrentHashMap中的续期状态 + 取消定时任务;客户端宕机则定时任务随进程终止,锁到期自动释放,杜绝死锁。

4、源码层:核心类 / 方法拆解(体现源码阅读能力)

挑 3 个核心点,不堆砌代码,只讲关键逻辑:

1、 入口方法:RedissonLock.lock() 进一步调用了 重载方法lock(-1, null, false),这里默认的 leaseTime=-1触发看门狗;

2、 续期核心:renewExpiration() 去 创建 NettyTimerTask,异步调用renewExpirationAsync()(Lua 脚本续期),成功则递归续期,失败则终止;

3、 状态管理:EXPIRATION_RENEWAL_MAP(ConcurrentHashMap)+ExpirationEntry 二者结合,实现 全局存储锁续期状态,记录线程 ID / 重入次数 / 定时任务,保证线程安全。

5、设计层:技术选型 + 设计思想(体现架构思维)

1、 为什么选 Netty TimerTask 而非 JDK Timer :

异步非阻塞,复用 Netty EventLoop 线程池,无额外资源开销;

异常隔离,单个任务失败不影响整体;

哈希轮算法 O (1) 性能,适配高并发场景;

2、 设计思想 / 模式 :

封装:把 Lua 脚本、续期逻辑、状态管理封装在RedissonLock,对外仅暴露lock/unlock;

观察者模式:future.whenComplete()异步回调处理续期结果;

防御式设计:1/3 提前续期、锁归属校验、宕机自动过期,杜绝死锁;

3、 为什么不用 Redis 键空间通知(被动续期) :

可靠性低:Pub/Sub 消息易丢失,续期不及时;

性能差:Redis 服务端推送大量通知,压力集中;

实时性弱:通知触发时锁可能已释放,需额外维护 “影子 Key”,复杂度高。

6、对比层:方案优劣(体现技术选型判断力)

横向对比,突出 Redisson 的设计优势:

方案

核心问题

Redisson 看门狗的优势

手动续期(守护线程)

线程泄漏、续期时机难把控、异常处理复杂

自动管理,多重校验,无手动编码成本

Redis 键空间通知

消息丢失、实时性差、服务端压力大

客户端主动续期,可靠性 100%,压力分散

固定过期时间

业务超时导致锁失效

动态续期,适配不确定时长的业务

7、收尾:总结核心价值(拔高认知)

Redisson 看门狗的本质是客户端侧的主动式、高可靠、低耦合的续期方案,通过这个方案:

既通过 Netty TimerTask 保证了续期的性能和可靠性

又通过封装和状态管理降低了开发者的使用成本,

通过防御式设计规避了死锁、续期异常等分布式锁的核心风险

这也是它成为生产环境分布式锁首选的核心原因。

8、回答技巧补充(高维暴击关键)

1、 避坑点:不要只讲 “看门狗是个线程续期”,要精准说 “Netty HashedWheelTimer 驱动的异步定时任务 + 递归续期”;

2、 细节加分:提到 “Lua 脚本保证续期原子性” “1/3 续期时机避免网络延迟导致续期不及时” “ConcurrentHashMap 保证多线程下续期状态安全”;

3、 反客为主:讲完核心逻辑后,可主动补充 “如果是 Redis 集群场景,Redisson 会通过 hash 算法定位 Master 节点加锁,续期也只针对该节点,保证集群下的锁一致性”,超出面试官预期。

“Redis 集群的 hash 算法是服务端为了数据分片设计的(CRC16 + 槽位),目的是把 Key 均匀存到不同节点;而 Redisson 的 hash 算法是客户端侧复用这个槽位逻辑,专门针对锁 Key 计算出固定的 Master 节点,保证同一把锁的加锁、续期、解锁都在同一个节点执行,避免集群环境下锁操作分散导致的一致性问题。”

说到这里,offer已经到手了!!!!!

尼恩架构团队塔尖的redis 面试题

[美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?]( 美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?)

京东面试: 亿级 数据黑名单 ,如何实现?(此文介绍了布隆过滤器、布谷鸟过滤器)

希音面试:亿级用户 日活 月活,如何统计?(史上最强 HyperLogLog 解读)

史上最全: Redis: 缓存击穿、缓存穿透、缓存雪崩 ,如何彻底解决?

史上最全: Redis锁如何续期 ?Redis锁超时,任务没完怎么办?

史上最全:Redis分布式 锁失效了,怎么办?

史上最全:Redis分段锁,如何设计?

Redis 锁的5个大坑,如何规避?

史上最全:Redis热点Key,如何 彻底解决问题

史上最全:为啥Redis用哈希槽,不用一致性哈希?

史上最全:如何保持 Redis 数据一致性?

希音面试:Redis脑裂,如何预防?你能解决吗?(看这篇就够了)

哈罗面试:Redis怎么模糊查询、Redis危险的命令 ?

阿里面试:Redis 为啥那么快?怎么实现 100W并发?说出 这 6大架构,面试官跪 了

阿里面崩:听说Redis Pipeline能提升3-12倍性能 ?怎么实现的?我懵逼了。 6抡暴击, 帮你KO 面试官

字节 面挂了 !问 “Redis 突然挂了, 为什么?”,我答 “淘汰策略” ,面试官: 你说说 底层原理、 监控指标?

更多创意

蜚声音响
365根据什么来封号

蜚声音响

📅 08-13 🔥 6981
揭秘地球直径:为什么地球是个“大胖子”?
365根据什么来封号

揭秘地球直径:为什么地球是个“大胖子”?

📅 10-08 🔥 2470
福特翼搏车怎么样
bt365无法登陆

福特翼搏车怎么样

📅 10-01 🔥 7073