这里我们主要看一下,JDK11下的Redisson,是如何实现加锁和进行锁的续期的
核心流程源码分析
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
// 尝试获取锁,如果拿到了锁,返回null,没到到锁,则返回当前锁的剩余时间
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
// 更新剩余等待时间
time -= System.currentTimeMillis() - current;
if (time <= 0) {
// 等待超时,处理获取锁失败
acquireFailed(waitTime, unit, threadId);
return false;
}
current = System.currentTimeMillis();
// 订阅锁释放的事件
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// 在这里阻塞掉当前等待拿锁的线程,如果超时,返回false
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
//对等待超时的处理
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
//取消订阅
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
}
try {
//重新计算等待时间
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 循环拿锁
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
//等待锁释放的事件
if (ttl >= 0 && ttl < time) {
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
// 取消锁消息的订阅
unsubscribe(subscribeFuture, threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}
加锁和续期核心代码
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
首先先看各个属性是什么:
- KEYS[1] 存的是你的定义的redis锁的key
- ARGV[1] 存的是你的持有锁的时间,也就是leaseTime
- ARGV[2] 存的是你的threadId
然后看看这个lua脚本干了什么: 第一个if:
- 判断你的redis锁的key是否存在
- 如果不存在,先给key下的ARGV[2]+1
- 然后给你的key下的ARGV[1]添加超时时间
- 然后返回nil,也就是null
这里有一个问题,就是threadId是被包装过的getLockName(threadId),这里填进去的值,其实是客户端的uuid+threadId
第二个if:
- 第一个key已经存在了,那么看这个key里ARGV[2]存的值是否等于客户端的uuid+threadId,这里其实就是一个可重入锁的实现了
- 如果等于当前线程,则给ARGV[2]+1
- KEYS[1],重新设置持锁时间,这里其实就已经刷新了锁的持锁时间
- 然后返回nil,也就是null
- 如果上面的if都不成立,则返回当前key的剩余失效时间
总结一下上面的lua脚本就是,这里定义了一个可重入锁,去拿锁的时候,没有锁就直接拿锁,设置持锁时间;有锁则判断是否当前线程持有,是的话 就刷新当前的持锁时间,给ARGV[2]+1,如果拿不到锁,就返回锁的剩余时间
自动续期实现
redisson看门狗自动续期又是如何实现的呢?一般看门狗机制启动,是在获取到锁以后,我们直接看tryAcquire()方法里的tryAcquireAsync()方法
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
// 当你的持锁时间不为-1时,是不会触续期的,因为拿锁后就直接返回了
if (leaseTime != -1) {
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
//自动续期的逻辑
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
这里可以看到,你的leaseTime只要不是-1,是不会触发自动续期的操作的,下面看一下自动续期的逻辑
private void renewExpiration() {
//EXPIRATION_RENEWAL_MAP这里维护着需要续期对象的列表,假如列表的对象被移除了
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
//这里的代码可以理解为 每持锁时间的1/3秒,回去执行一次续期
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;
}
// 续期的逻辑,主要检查锁是不是由当前线程持有,是的话返回true
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);
ee.setTimeout(task);
}
上面的代码可以看到,Redisson内部维护了一个需要续期的Map,通过去这个map中找当前线程的锁,还在不在这个map里,如果在,就给这个key续期, 不在了,那么内部的递归就有出口了,可以跳出递归的循环
总结
Redisson是如何实现分布式锁以及锁的自动续期的?
- 首先是Lua脚本,保证加锁操作操作的原子性
- 拿锁失败的线程订阅所释放的事件,来进行线程的阻塞和唤醒
- 维护一个需要续期的Map对象EXPIRATION_RENEWAL_MAP,用于存储需要续期的对象
- 续期时,在EXPIRATION_RENEWAL_MAP根据当前线程id查有没有值,有,说明需要续期,通过Lua脚本给锁续期,然后就递归自己,重新走一遍逻辑, 直到当前线程在EXPIRATION_RENEWAL_MAP拿不到值为止
- 在释放锁的时候,会删除当前线程在EXPIRATION_RENEWAL_MAP中的对象