Redisson分布式锁原理的探究,不是你想的那样
背景
作为Java开发,聊到分布式锁
,大家第一反应该都会想到 Redis吧,基本思路用SetNX + Lua脚本实现,但都有些问题例如锁过期了业务还没执行完、释放别人的锁。
理想的方案是用Redisson
,网上的说法是Redission加锁后,会有一个异步线程定时检查,如果锁还存在,则给锁续期。
如果你没看过具体实现,你肯定有疑问
问题
- Redisson 是加一把锁成功后,就另起一个异步线程自动续期吗?
- 锁释放后,续期任务如何取消呢?
带着问题分析源码
总体
测试代码
ini
RLock lock = redissonClient.getLock("分布式锁");
lock.tryLock();
tryLock() 方法一直往下点,最终来到 RedissonLock类的 tryAcquireOnceAsync()方法,我已对核心逻辑做了注释
scss
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Boolean> acquiredFuture;
//如果手动设置了锁的过期时间
if (leaseTime > 0) {
acquiredFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
} else {
//这是默认加锁逻辑,也就是默认设置锁30s
acquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
//异步编程
CompletionStage<Boolean> f = acquiredFuture.thenApply(acquired -> {
//如果加锁成功
if (acquired) {
if (leaseTime > 0) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
//默认逻辑:加锁成功后,启动一个给锁的续期任务(看门狗)
scheduleExpirationRenewal(threadId);
}
}
return acquired;
});
return new CompletableFutureWrapper<>(f);
}
加锁原理
继续查看源代码 默认加锁的方法 。
ini
acquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
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]);
可以看到,加锁是通过Hash结构 和Lua脚本实现。 其中Hash的 key是锁的名称,field是线程名字,value是所重入次数。
加锁逻辑如下
-
判断 分布式锁名称 key是否存在
- 如果不存在,则设置 这个锁,然后所重入次数 + 1。
-
如果存在,则判断field 是否为当前线程
- 如果不是,说明别的线程已经抢到锁了。直接返回加锁失败
- 如果是,则重入次数加一。
续期原理-----看门狗机制
在 scheduleExpirationRenewal(threadId)
方法里的 renewExpiration() 方法。 这是源码,注释我已写好
ini
private void renewExpiration() {
//
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
//这段代码的含义,就是向时间轮添加一个10s的延迟任务,具体就是给锁续期。
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//这是一个ConcurrentHashMap,再前文加锁成功后,会向里面put()一个值
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);
// 从map里移除相关信息,执行任务时发现为 null,就退出方法。
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
//如果业务还在执行中,则递归执行该方法,再向时间轮添加一个续期任务。
if (res) {
renewExpiration();
} else {
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
默认 key 过期时间 30s,然后定时任务每 10 秒( lockWatchdogTimeout/3 )进行一次调用,执行锁续期动作,若这个线程还持有这个锁,就对这个线程持有的锁进行续期操作(通过 pexpire 续期 key 30s)。
当业务执行完,解锁操作时,会调用ConcurrentHashMap.remove( getEntryName()),这样续期方法就直接退出了。
总体图
回到最初的问题
- Redisson 是加一把锁成功后,就另起一个异步线程自动续期吗?
很明显不是,一个线程管理所有分布式锁的续期任务
- 锁释放后,续期任务如何取消呢?
从map里移除相关信息,执行任务时发现为 null,就退出方法。(在上面的代码注释已经写得很清楚了)