redisson锁重试和看门狗机制
什么是锁重试机制?
如果一个线程获取不到锁不会立即失败而是在一段时间内继续尝试获取锁。
为什么要有锁重试机制?
在一些业务中,不希望获取失败之后立刻放回失败。
怎么实现锁重试机制?
Redisson已经实现了,具体代码附上解释如下
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
// 同一等待时间单位为毫秒
long time = unit.toMillis(waitTime);
// 获取当前时间
long current = System.currentTimeMillis();
// 获取线程id
long threadId = Thread.currentThread().getId();
// 尝试获取锁,方法放回两种结果,如果是null表示获取成功,如果是ttl(锁剩余时间),表示获取失败
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// 如果成功过,返回true
if (ttl == null) {
return true;
}
// 如果失败,尝试再次获取,先获取上面消耗了多少时间
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
// 如果等待时间已过,返回false
return false;
}
// 不会立刻尝试获取,刚刚获取不到,立马获取大概率获取不到
current = System.currentTimeMillis();
// 调用异步方法,订阅消息,如果有锁释放,尝试获取
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// 调用await等待异步方法,两种结果,一种是等不带订阅信息,取消等于放回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);
if (ttl == null) {
// 成功获取
return true;
}
// 获取失败,判断是否超时
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 阻塞等待订阅信息,两种结果,一种超时放回false,一种接收到,返回true
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);
}
}
为什么要使用看门狗机制
在Redisson实现分布式锁,使用开门狗机制更新锁的过期时间。
怎么实现看门狗机制?
Redisson中是这样实现的:
在tryLock方法中,可以有三个参数,一个是重试等待时间(-1,默认是不等待),一个是锁过期时间(-1,默认是30秒),一个是时间的单位(null,默认是毫秒),如果我们的锁过期时间未设置,就会启用看门狗机制,过期时间是30秒,锁获取后,在释放之前,定时任务每十秒更新锁的过期时间成30秒,确保如果我们没有设置过期时间,出现问题未手动释放锁的时候能够(崩溃)能够自己释放锁。
这是看门狗机制实现的部分代码
private void scheduleExpirationRenewal(long threadId) {
// 创建一个ExpirationEntry
ExpirationEntry entry = new ExpirationEntry();
// 这个MAP是静态变量,它的所有实例都能看到这个map,能欧在这个map获取数据
// getEntryName()是锁的名称,一个锁对应一个创建一个ExpirationEntry
// putIfAbsent是不存在的时候才能设置,不存在放回null,存在放回entry
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
// 为null表示已经设置过了,不是第一次获取
if (oldEntry != null) {
// 将id添加进去
oldEntry.addThreadId(threadId);
} else {
// 将id添加进去
entry.addThreadId(threadId);
// 启动定时任务,更新有效时间
renewExpiration();
}
}
定时任务
private void renewExpiration() {
// 获取这个锁的entry对象
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;
}
// 更新有效期
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
// 递归调用
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
在释放锁的时候需要关闭定时任务