【本篇文章基于redisson-3.17.6版本源码进行分析】
为什么需要自动续期?
设想一下,如果我们指定的锁的超时时间是30秒,但是业务执行在30秒内还没有执行完成,此时分布式锁超时过期自动释放,其它线程就能获取到这把锁,这样就有问题了。
为了保证业务执行完后,锁才能够释放,Redisson提供了看门狗(watch dog)机制,实现了锁的自动续期。
本文就一起看看加锁成功之后的看门狗是如何实现的?
我们再回来看Redisson中异步加锁方法tryAcquireAsync():
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime > 0) {
// 指定了超时时间
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
// 未指定超时时间 internalLockLeaseTime默认就是看门狗的超时时间:30秒
// waitTime: -1
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
// lock acquired
// ttlRemaining为null, 其实就是加锁的LUA脚本中返回的nil,表示获取锁成功
if (ttlRemaining == null) {
if (leaseTime > 0) { // 如果设置了超时时间,则更新internalLockLeaseTime为指定的超时时间,并且不会启动看门狗
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
// 自动续期实现,看门狗机制入口
/**
* 1. 只有用户未指定锁的超时时间,看门狗才生效;如果我们指定了锁超时时间,则看门狗不会启动;
* 2. 获取锁成功的线程才启动看门狗
* 例:
* lock.lock(); 开启看门狗
* lock.lock(5000, TimeUnit.SECONDS); 不开启看门狗
*/
scheduleExpirationRenewal(threadId);
}
}
return ttlRemaining;
});
return new CompletableFutureWrapper<>(f);
}
通过前面文章的分析,我们知道,当加锁的LUA脚本返回nil时,表示的就是加锁成功了。
对应到上面的代码就是tryLockInnerAsync()方法的返回值是null:
从源码中可以看到,并不是所有情况下,Redisson都会启动看门狗自动续期的。
只有同时满足下面两个条件,Redisson才会启动看门狗机制:
- 1、当前线程获取锁成功;
- 2、未指定锁的超时时间,看门狗才生效;如果我们指定了锁超时时间,则看门狗不会启动;
接下来我们进入scheduleExpirationRenewal(threadId)方法,看名字是到期续订的意思。
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);
try {
// 自动续期
renewExpiration();
} finally {
if (Thread.currentThread().isInterrupted()) {
// 如果当前线程被中断,则取消看门狗的自动续期
cancelExpirationRenewal(threadId);
}
}
}
}
进入renewExpiration(),看门狗的重要逻辑都在这个方法:
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
// 缓存为空,则不需要自动续期,直接返回
if (ee == null) {
return;
}
/**
* 基于Netty时间轮实现自动续期,实际上就是一个延迟定时任务
* 【internalLockLeaseTime / 3】: 第一次续期是在【过期时间 / 3】时执行,默认是每10秒将锁的过期时间续期为30秒
*/
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;
}
// 执行自动续期的核心代码
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getRawName() + " expiration", e);
// 报异常,从缓存中移除key, 下一次就不会续期了.
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
// res为true的话,表示自动续期成功,继续递归调用,实现不断续期
if (res) {
// 递归调用,实现不断续期
// reschedule itself
renewExpiration();
} else {
// 续期失败,则取消定时任务, 移除key等
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
从源码中可以看到,Redisson的看门狗借助了netty的时间轮,简单理解就是定时任务,对分布式锁进行自动续期的。
有几个关键点:
- 1、延迟调度,延迟时间为:internalLockLeaseTime / 3,就是 10s 左右后会调度这个 TimerTask;
本例中,我们没有指定超时时间,internalLockLeaseTime默认就是在RedissonLock构造方法中获取的看门狗超时时间30秒,那么30 / 3 = 10秒,也就是看门狗将会延迟10秒启动第一次续期。
- 2、异步续租:逻辑都在renewExpirationAsync(threadId)里面,后面详细分析;
- 3、递归调用:当续租成功之后,重新调用renewExpiration()自己,从而达到持续续租的目的;
接下来我们看一下看门狗是如何实现自动续期的,核心代码在renewExpirationAsync(threadId)方法:
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
/**
* 底层还是通过LUA脚本实现: 通过hexists指令判断锁是不是自己的锁,如果是的话,则通过pexpire指令将锁的过期时间给重置了,返回1,表示自动续期成功;返回0,表示续期失败。
*/
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脚本,通过hexists指令判断锁是不是自己的锁,如果是的话,则通过pexpire指令将锁的过期时间给重置为30秒,返回1,表示自动续期成功;返回0,表示续期失败。
过了 10s 左右,判断到线程还持有着这把锁,即业务还没执行完,就会将锁的时间重新设置为 30s,返回true,然后通过递归,又过了10s,再一次续期,不断循环这个过程,直到锁被释放或者其它一些情况判断到当前线程已经没有持有这把锁之后,取消看门狗定时任务。
简要总结一下看门狗机制:
- 只有在未指定锁超时时间时才会使用看门狗;
- 看门狗默认续租时间是 10s 左右,internalLockLeaseTime / 3;
- 可以通过 Config 统一设置看门狗的时间,设置 lockWatchdogTimeout 参数即可;