Redisson看门狗机制

原理

就是开一个定时任务,,然后每隔10s检查一次,并将其续约恢复至我们设置的lockWatchdogTimeout(默

认是30s)

jvm挂了,看门狗会一直续期下去吗

挂了的话,不会一直续期的。看门狗是jvm线程,jvm挂了的话,会终止续期的

加锁线程挂了,但jvm没挂,看门狗会一直续期下去吗

看门狗线程本质就是一个守护线程,且这个线程是和加锁线程绑定的,或者说它续期会去判断加锁线程是否存

活,存活才会去续期

并非守护线程

源码解读

续期任务调度的实现

这个方法是为锁开启续期任务,且一把锁只会有一个续期任务!!!由第一个持有该锁的线程开启,后续第

n个持有该锁的线程只需要把threadId注册到一个concurrentHashMap里

为什么?

你续期任务本质就是一个开一个线程不断轮询,那一个线程配一个定时线程这显然是不合理的。肯定

是不合理的,只需要一把锁对应一个续期任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected void scheduleExpirationRenewal(long threadId) {
//new一个续期任务
ExpirationEntry entry = new ExpirationEntry();
//EXPIRATION_RENEWAL_MAP:concurrentHashMap k:锁 v:一个entry field有线程ID
//Q:为什么要为concurrentHashMap来保证线程安全呢
//A:因为它是Redisson所有锁的基类,像读写锁这种,可能会有多个线程共同持有锁的情况
//如果这个锁已经有一个续期任务在执行了,就不覆盖,返回旧的 entry;否则返回 null,表示可以放入
ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
//不为空,说明这把锁已经有续期任务了
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
//首次为这把锁开 续期任务 并把当前线程注册到
entry.addThreadId(threadId);

try {
//执行续期
this.renewExpiration();
} finally {
//如果线程被中断了 就取消续期
//Q:什么情况下,线程会被中断?
if (Thread.currentThread().isInterrupted()) {
this.cancelExpirationRenewal(threadId);
}

}
}

}

续期的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* Redisson 分布式锁 —— 看门狗续期核心逻辑
*
* 功能:
* 1. 定期(默认锁租期的 1/3)检查锁是否仍然被当前线程持有;
* 2. 若锁仍在,则延长过期时间(防止业务执行过长锁被自动释放);
* 3. 若锁已不存在,则取消续期任务;
* 4. 若发生异常(如 Redis 宕机),则清理当前锁的续期状态。
*
* 实现细节:
* - 使用 Netty 的时间轮(HashedWheelTimer)定时调度任务;
* - 使用异步命令(CompletionStage)执行 Redis 脚本;
* - 通过回调 whenComplete 处理续期结果或异常。
*/
private void renewExpiration() {
// 获取当前锁对应的续期任务信息(存放于全局 Map)
ExpirationEntry ee = (ExpirationEntry) EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
// 使用 Netty 时间轮延迟执行任务(间隔 = 租期时间 / 3)
//newTimeout:
Timeout task = this.getServiceManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// 再次确认该锁的续期任务仍存在(防止任务被取消)
ExpirationEntry ent = (ExpirationEntry) RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
if (ent != null) {
// 获取该锁第一个持有线程的 threadId(可能是重入锁)
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
/**
* 续期逻辑:
* 调用 Lua 脚本检测锁是否仍属于当前线程,
* 若是,则执行 pexpire 延长过期时间;
* 返回值:
* true -> 锁还在,续期成功;
* false -> 锁已不存在或非本线程持有。
*/
CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);

/**
* Q:为什么是异步?
* A:
* - renewExpiration() 是周期性执行的;
* - 如果同步等待 Redis 响应,Netty 时间轮线程会被阻塞;
* - 异步执行可以充分利用线程池,提高续期稳定性;
* - 防止 Redis 网络抖动造成时间轮延迟。
*/

// 注册异步回调:处理续期结果与异常
future.whenComplete((res, e) -> {
/**
* e:表示异步任务执行过程中发生的异常(如 Redis 连接错误、脚本超时等)
* res:表示 Redis 脚本返回结果(true=锁还在,false=锁不存在)
*/
if (e != null) {
// 出现异常,打印日志 + 清理续期任务(避免死循环)
RedissonBaseLock.log.error(
"Can't update lock {} expiration",
RedissonBaseLock.this.getRawName(), e
);
RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
} else {
if (res) {
// res == true:锁仍被当前线程持有,继续下一次续期
RedissonBaseLock.this.renewExpiration();
} else {
// res == false:锁不存在(可能已释放或超时),停止续期任务
RedissonBaseLock.this.cancelExpirationRenewal(null);
}
}
});
}
}
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);

// 将定时任务句柄存回 ExpirationEntry(便于后续取消)
ee.setTimeout(task);
}
}

Redisson的定时任务是基于Netty中的时间轮来实现的

时间轮是什么?时间轮算法

Redisson执行定时任务的work线程

timer就是一个HashedWheelTimer(Netty的时间轮)

return 一个Timeout(表示你刚刚注册的这个定时任务,你可以通过这个timeout来查看任务状态等等)

newTimeout:往时间轮里面 注册一个定时任务