回顾下怎么加锁
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 ()都有看门狗,因为看到条件判断 lease Time =-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();
}
}
时间传-1就会开启看门狗机制
现在知道 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 脚本在 renewExpirationAsync 里
- 续期成功自己调用自己,也就是为下一次续期做准备
- 续期失败就取消续期,移除 key 等操作
- 续期的开始时间是超过过期时间的三分之一,比如9s过期时间,那么第3s的时候开始续期。
所以重点看下续期的 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,也就是 alse 。
那就是返回1就调用自己准备下一次续期: renewExpiration ();,返回0就调用 cancelExpirationRenewal ( null );取消续期,删除 key 等操作。
需要注意的点:
- watchDog 并不是全部 lock 都生效,而是 lock 没设置过期时间的那些锁才会开启 watchDog 续期,没设置过期时间的话默认采取的是 watchDog 的30s过期时间。如果调用 lock ( time , unit )是不会开启 watchDog 线程续期的,是有可能造成线程不安全的。
- 续期是段 lua 脚本。
- 续期线程会在续期时间超过三分之一的时候执行。
疑问:不会浪费性能吗?每个方法都起个看门狗线程,这个影响有多大?
比篇讲述了看门狗是怎么工作的,他的核心原理我们一清二楚,整个加锁的这块流程算是告一段落了,接下来我们需要知道锁是怎么释放
的?下篇分析!