Redisson分布式锁流程分析(2)看门狗

前面加锁的文章中提到了如果不传加锁时间,默认使用30s,而并不是永久时长,拿到了30s后难道锁就会自动释放吗?答案是不会,因为Redisson中还有一个耳熟能详的看门狗(Watchdog)机制。
本文就一起看看加锁成功之后的看门狗(Watchdog)是如何实现的?

加锁成功

在前一篇文章中介绍了可重入锁加锁的逻辑,其中 RedissonLock 类的 tryAcquireAsync 方法是进行异步加锁的逻辑。

回顾一下调用lock()方法时tryAcquireAsync这个方法的入参:

  • waitTime:-1;
  • leaseTime:-1,加锁时未指定锁时间,则为 -1,如果指定,则是指定的时间;
  • unit:null;
  • threadId:当前线程 id。

其中的 tryLockInnerAsync 在前面那篇文章已经介绍过了。当加锁成功时,该会返回 null,加锁失败,会返回当前锁的剩余时间。

所以如果加锁成功,就会进入到红框标记的代码部分。

因为 leaseTime 的值为-1,所以会进入到 scheduleExpirationRenewal 方法,也就是这篇文章的主题:看门狗。

此外根据 leaseTime 的判断,还可以得出一个结论: Redisson 看门狗(Watchdog)在指定加锁时间时,是不会对锁时间自动续租的。只有不指定加锁时间才会续租。

看门狗

scheduleExpirationRenewal方法位于RedissonLock的父类RedissonBaseLock类中,并且最终会调用到该类中的renewExpiration()方法。

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

protected void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
renewExpiration();
}
}

private 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);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}

if (res) {
// reschedule itself
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

ee.setTimeout(task);
}

看门狗的一部分重点逻辑就在 renewExpiration 方法这里:

  1. 延迟调度,延迟时间为:internalLockLeaseTime / 3,就是 10s 左右后会调度这个 TimerTask;
  2. 异步续租:逻辑都在 renewExpirationAsync 里面;
  3. 递归调用:当续租成功之后,重新调用 renewExpiration 自己,从而达到持续续租的目的;
  4. 当然也不能一直无限续租,所以中间有一些判断逻辑,就是用来中断续租的。

续租逻辑

1
2
3
4
5
6
7
8
9
10
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}

可以看到续租也是异步去执行一段 lua 脚本,当key和field都存在时,将redis中的key重新设置时间。 这样执行下来,将会在过了 10s 左右后将锁的时间重新设置为 30s。

总结

至此,看门狗介绍完毕,简要总结一下内容。

  1. 只有在未指定锁超时时间时才会使用看门狗;
  2. 看门狗默认续租时间是 10s 左右,internalLockLeaseTime / 3;
  3. 可以通过 Config 统一设置看门狗的时间,设置 lockWatchdogTimeout 参数即可。

最后,同样使用一张图,进行下总结:

------ 本文完 ------