Redisson是怎么实现可重试锁的
可重试锁是什么:一个请求尝试获取锁时,如果锁已被人占用,则立即返回失败结束请求是不合理的。最好的方式是给予一定的容错空间,在一定的时间内再多次尝试获取锁,如果长时间仍获取不到才返回失败
。而简单的基于redis实现的分布式锁不能实现该功能。redisson可以实现该功能。
逻辑原理:
上锁的时候会设定一个等待超时的时间,例如3s;第一次获取锁失败,会判断是否超过3s可等待时间,如果未超过,也不能一直不间断的重试,这样只会徒劳占用资源,而是应订阅释放锁的信号,如果在等待时间范围内订阅到信号,则再去尝试获取锁。超过该时间仍未获取则返回失败。
逻辑图如下:
下面我们进入redisson上锁源码:
可以看到调用tryLock()
方法时,第一个可传参就是等待时间
进入tryLock(long time, TimeUnit unit)
方法,选择RedissonLock
实现类,最后会调用this.tryLock(waitTime, -1L, unit)
方法,进入方法:
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();
//【1 尝试获取锁】
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();
//【2 订阅释放锁的信号。在释放锁的时候会再删除锁之后发布一条通知】
CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
try {
subscribeFuture.get(time, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
if (!subscribeFuture.completeExceptionally(new RedisTimeoutException(
"Unable to acquire subscription lock after " + time + "ms. " +
"Try to increase 'subscriptionsPerConnection' and/or 'subscriptionConnectionPoolSize' parameters."))) {
subscribeFuture.whenComplete((res, ex) -> {
if (ex == null) {
unsubscribe(res, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
} catch (ExecutionException e) {
acquireFailed(waitTime, unit, threadId);
return false;
}
//【重试阶段】
try {
time -= System.currentTimeMillis() - current;
if (time <= 0) {
//若此时已经超时,则失败
acquireFailed(waitTime, unit, threadId);
return false;
}
//【3 循环重复尝试】
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();
//返回的ttl是缓存中被人占用的key剩余的过期时间
if (ttl >= 0 && ttl < time) {
//【4 等待获取信号量】
//如果剩余释放时间只剩2s,等待时间还有3秒,则只需要等待2s即可
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
//等待time时间
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
//如果这时候还有时间则在进入循环
}
} finally {
//取消订阅
unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
}
}
可以看到,程序会先尝试获取锁见步骤【1】,如果获取失败就要准备循环尝试再次获取。但也不能一直不间断的循环,那样只会空耗资源。所以见步骤【2】,订阅释放锁的消息(在解锁的时候会发布这个订阅)。订阅完成后进入步骤【3】再次尝试获取,如果失败,进入步骤【4】等待获取发布的订阅信息。最长的等待时间不会超过传入的等待时间time。如果接收到订阅信号,且此时还没超时,就再循环尝试获取锁,也有可能别的请求速度更快抢占了锁,则只能再次进入等待方法;如果超时则退出。