前言
看门狗的定义,在之前一个小章节中已经有简单说明,这里不做赘述。但是之前只是说了其与持有时间,等待时间的区别,没有详细介绍,下面我们来进行深入分析,解开迷雾。
设置看门狗时间:org.redisson.config.Config.setLockWatchdogTimeout(30 * 1000);
看门狗作业流程
假设看门狗时间为30秒【默认】,如果线程没有设置超时时间,那么设置internalLockLeaseTime时间为看门狗时间。在线程持有锁之后,每隔1/3 * internalLockLeaseTime,会对锁进行续命,每次续命的时间为internalLockLeaseTime.
RedissonLock中有个静态内部类,ExpirationEntry,一个锁资源对应一个entry实例,保存在一个静态Map集合中【EXPIRATION_RENEWAL_MAP】,entry里面存储了当前锁资源里持有锁的所有线程id集合【threadIds】,定时任务会遍历此线程id集合,对其持有的资源进行续命。
问题:一个资源只有一个ExpirationEntry实例,为什么会有一个线程集合?
原因其实可以想到,之前讲解过了Redisson的读写锁,其中读锁是共享锁类型,会有多个线程id持有同一个锁资源。
如何生效
如果没有设置持有时间,那么表示锁是不会超时的,这时就用到看门狗了
scss
private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
// 没有设定持有时间,将其设置为看门狗时间
RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// 获取锁成功之后,开启定时任务
if (ttlRemaining) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
执行步骤:
- 判断是否设定了持有时间,如果没有,则将持有时间设定为看门狗时间;
- 持有成功之后,开启定时任务,参数为当前线程id;
重入设定
持有锁成功之后,判断此线程id是否已经保存在当前jvm进程中【scheduleExpirationRenewal】
ini
private void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
// entryName是当前锁资源的描述,entryName = id + ":" + name;
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
renewExpiration();
}
}
执行步骤:
- 在EXPIRATION_RENEWAL_MAP中,判断当前资源是否已经存在,如果存在,表示已经开启了定时任务,以免定时任务重复。然后将线程id添加到ExpirationEntry的threadIds集合中,定时任务会遍历执行;
- 如果资源不存在,则添加线程id之后,直接开启;
如何续命
遍历集合中的线程id,执行续命操作【renewExpiration】
关键代码
ini
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 " + getName() + " expiration", e);
return;
}
if (res) {
// reschedule itself
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
// 将定时任务设置到entry实例中,timeout属性在entry对象中,是volatile的,可见其考虑到了解锁时,是否能及时拿到此task的可能性。
ee.setTimeout(task);
}
执行步骤:
- 判断Map集合中,是否存在锁资源,避免其他线程解锁时,在集合中删除此实例;
- 如果存在,则开启定时任务;
- 定时任务中,再次判断锁资源是否为空和线程id是否为空,如果为空,则结束定时任务;
- 执行成功后,开启下一次定时任务,定时任务周期为1/3看门狗时间;
手动续命
根据参数里设定的时间【internalLockLeaseTime】来重置锁过期时间
typescript
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getName(), 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(getName()),
internalLockLeaseTime, getLockName(threadId));
}
执行步骤:
- 判断锁资源是否存在,如果存在,则重置锁过期时间,时间为看门狗时间;
- 如果不存在,直接返回false;
取消续命任务
当解锁成功后【重入锁需要将持有次数减为零】,取消续命定时任务;
ini
void cancelExpirationRenewal(Long threadId) {
ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (task == null) {
return;
}
if (threadId != null) {
task.removeThreadId(threadId);
}
if (threadId == null || task.hasNoThreads()) {
Timeout timeout = task.getTimeout();
if (timeout != null) {
timeout.cancel();
}
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
}
执行条件:当解锁成功后【重入锁需要将持有次数减为零】,取消续命定时任务;
执行步骤:
- 判断entry对象是否为空,为空直接返回,表示没有续命定时任务【如果拿锁时,设定了持有时间,就不会有续命定时任务】;
- 取出entry对象中的定时任务,如果不为空则取消;
- 删除EXPIRATION_RENEWAL_MAP集合中的entry对象。