尼恩说在前面
在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包的依赖
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
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.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
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
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
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 突然挂了, 为什么?”,我答 “淘汰策略” ,面试官: 你说说 底层原理、 监控指标?