作者: 编程界的小学生
日期: 2021/09/09
修订: 初版,未修订。2021/09/09
版权: 内部资料,切勿泄漏,违者必究。
上一篇讲解了加锁的核心流程、可重入是怎么做的以及互斥性是怎么实现的,但是如果业务代码没执行完锁却过期了,这时候怎么办?这不就线程不安全了吗?别急,Redssion内部有个看门狗机制,WatchDog!
如果业务代码没执行完,锁却过期了,这时候其他线程又能抢锁了,线程不安全啦。所以Redisson内部有个看门狗的机制,意思是定时监测业务是否执行结束,没结束的话你这个锁是不是快到期了(超过锁的三分之一时间,比如设置的9s过期,现在还剩6s到期),那就重新续期。这样防止如果业务代码没执行完,锁却过期了所带来的线程不安全问题。
回顾下怎么加锁的?lock()!
RLock lock = redisson.getLock("myLock");
lock.lock();
lock()干了啥?
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// 加锁成功
if (ttl == null) {
return;
}
// 加锁失败,while(true)等待重试。
}
可以看到lock主要是请求tryAcquire(-1, -1, null, threadId)
来完成加锁逻辑,然后判断加锁成功与否,失败的话就重试。目前还没发现watchDog的机制,那我们继续追下去,看看如何加锁的?
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
// watchDog机制在这里
// -1, -1, null,threadId
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Boolean> ttlRemainingFuture;
// 省略一些请求lua加锁代码。之前都分析过。
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
// ttlRemaining为true代表加锁成功。
if (ttlRemaining == null) {
// 接下来是什么鬼逻辑?
// leaseTime == -1就scheduleExpirationRenewal开启看门狗续期,
// leaseTime != -1就不续期,只是把internalLockLeaseTime时间变成传进来的时间。
// 这里疑点重重:
// 1.什么时候leaseTime != -1?
// 2.不是所有的lock()方法都有看门狗机制?
if (leaseTime != -1) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
意外收获!好像不是所有的lock()都有看门狗,因为看到条件判断leaseTime == -1的时候才开启看门狗线程,不等于-1的时候就没有这个机制。那什么时候不等于-1呢?
回答这个问题前我们可以先推测下,-1是哪来的?是lock()入口默认带来的:
@Override
public void lock() {
try {
// -1 !!!
lock(-1, null, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
那我们是不是懂了?他绝逼有带时间参数的lock()方法。确实有:
@Override
public void lock(long leaseTime, TimeUnit unit) {
try {
lock(leaseTime, unit, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
搜噶,原来用户加锁lock的时候可以自定义时间,这个时间是干嘛的?加锁成功后等待这个时间就过期,不管你业务是否执行完成,因为没有看门狗机制。比如你设置的1s,业务执行了2s,那中途锁过期了,不会续期,可能造成线程不安全。
现在知道watchDog何时生效了,那继续看下他是怎么工作的?
上文可以发现续期的代码在这个方法里面:scheduleExpirationRenewal(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;
}
// 调用lua脚本进行续期
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
// 报异常就移除key
if (e != null) {
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
// 续期成功的话就下一轮续期。
if (res) {
// reschedule itself
renewExpiration();
} else {
// 续期失败的话就取消续期,移除key等操作
cancelExpirationRenewal(null);
}
});
}
// 这里是个知识点,续期线程在过期时间达到三分之一的时候工作,比如9s过期时间,那么续期会在第3秒的时候工作,也就是还剩余6s的时候进行续期
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
这里有四个关键点:
所以重点看下续期的lua源代码:
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));
}
很简单,就是看当前线程有没有加锁hexists, KEYS[1], ARGV[2]) == 1
,有加锁的话就代表业务线程还没执行完,就给他的锁重新续期pexpire', KEYS[1], ARGV[1]
,然后返回1,也就是true,没加锁的话返回0,也就是false。
那就是返回1就调用自己准备下一次续期:renewExpiration();
,返回0就调用cancelExpirationRenewal(null);
取消续期,删除key等操作。
需要注意的点:
疑问:不会浪费性能吗?每个方法都起个看门狗线程,这个影响有多大?
此篇讲述了看门狗是怎么工作的,他的核心原理我们一清二楚,整个加锁的这块流程算是告一段落了,接下来我们需要知道锁是怎么释放的?下篇分析!