tryAcquireAsync方法就是上面解读Redission的可重入锁的源码调用到的一个方法
上面有说到,没传leaseTime(自动释放锁时间)的话,就会给一个默认值,这个默认值就是getLockWatchdogTimeout(),也就是看门狗超时时间
这个看门狗超时时间是30*1000毫秒,也就是30秒
tryAcquireOnceAsync-》里面的 ()
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Boolean> acquiredFuture;
// 如果 leaseTime 大于 0,调用 tryLockInnerAsync 方法尝试获取锁,指定等待时间和租期时间
if (leaseTime > 0L) {
acquiredFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
} else {
// 如果 leaseTime 小于等于 0,调用 tryLockInnerAsync 方法尝试获取锁,使用内部锁租期时间
acquiredFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
}
// 处理获取锁的异步结果,确保在没有同步的情况下正确处理
CompletionStage<Boolean> handledFuture = this.handleNoSync(threadId, acquiredFuture);
// 添加后续处理逻辑,在锁成功获取后执行相应操作
CompletionStage<Boolean> f = handledFuture.thenApply((acquired) -> {
if (acquired) {
if (leaseTime > 0L) {
// 如果租期时间大于 0,更新内部锁租期时间
this.internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
// 如果租期时间小于等于 0,计划续期任务
this.scheduleExpirationRenewal(threadId);
}
}
return acquired;
});
// 将 CompletionStage 包装成 RFuture 返回
return new CompletableFutureWrapper<>(f);
}
如果获取锁返回的锁key的剩余有效期的时间为null的时候(也就是获取锁成功),就会解决最长等待剩余有效期的问题
protected void scheduleExpirationRenewal(long threadId) {
// 创建一个新的 ExpirationEntry 实例
RedissonBaseLock.ExpirationEntry entry = new RedissonBaseLock.ExpirationEntry();
// 尝试将新创建的 ExpirationEntry 放入 EXPIRATION_RENEWAL_MAP,如果已经存在条目则返回旧的条目
RedissonBaseLock.ExpirationEntry oldEntry = (RedissonBaseLock.ExpirationEntry) EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
if (oldEntry != null) {
// 如果旧的条目存在,将线程 ID 添加到旧条目中
oldEntry.addThreadId(threadId);
} else {
// 如果旧条目不存在,将线程 ID 添加到新条目中
entry.addThreadId(threadId);
try {
// 尝试续约锁的过期时间
this.renewExpiration();
} finally {
// 如果当前线程被中断,取消续约
if (Thread.currentThread().isInterrupted()) {
this.cancelExpirationRenewal(threadId);
}
}
}
}
private void renewExpiration() {
// 从 EXPIRATION_RENEWAL_MAP 获取当前锁的 ExpirationEntry
RedissonBaseLock.ExpirationEntry ee = (RedissonBaseLock.ExpirationEntry) EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
// 创建一个新的 Timeout 任务,并设定任务在 internalLockLeaseTime 的三分之一时间后执行
Timeout task = this.getServiceManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
// 在任务运行时,再次从 EXPIRATION_RENEWAL_MAP 获取当前锁的 ExpirationEntry
RedissonBaseLock.ExpirationEntry ent = (RedissonBaseLock.ExpirationEntry) RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
if (ent != null) {
// 获取第一个线程 ID
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
// 异步续约锁过期时间
CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
// 注册回调函数,处理续约结果
future.whenComplete((res, e) -> {
if (e != null) {
// 如果续约过程中发生异常,记录错误日志并从 EXPIRATION_RENEWAL_MAP 中移除当前锁条目
RedissonBaseLock.log.error("Can't update lock {} expiration", RedissonBaseLock.this.getRawName(), e);
RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
} else {
if (res) {
// 如果续约成功,递归调用 renewExpiration 方法,继续续约
RedissonBaseLock.this.renewExpiration();
} else {
// 如果续约失败,取消续约
RedissonBaseLock.this.cancelExpirationRenewal((Long) null);
}
}
});
}
}
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
// 将创建的 Timeout 任务设置到 ExpirationEntry 中
ee.setTimeout(task);
}
}
因此,一个锁就对应自己的一个ExpirationEntry类
这样,如果是第一次进入的时候,这里放进去的就是一个全新的ExpirationEntry类,也就是当前锁,返回值就是null
如果是重入,那么putIfAbsent函数就不会执行,返回值就是之前旧的ExpirationEntry类,也就是第一次进来创建的ExpirationEntry类。这一步保证了无论重入几次,拿到的都是同一把锁。
如果是第一次,那么需要进行续约操作,也就是给最长等待有效期续约。
总结
在使用Redis实现分布式锁的时候,会存在很多问题。
比如说业务逻辑处理时间>自己设置的锁自动释放时间的话,Redis就会按超时情况把锁释放掉,而其他线程就会趁虚而入抢夺锁从而出现问题,因此需要有一个续期的操作。
并且,如果释放锁的操作在finally完成,需要判断一下当前锁是否是属于自己的锁,防止释放掉其他线程的锁,这样释放锁的操作就不是原子性了,而这个问题很好解决,使用lua脚本即可。
Redisson的出现,其中的看门狗机制很好解决续期的问题,它的主要步骤如下:
在获取锁的时候,不能指定leaseTime或者只能将leaseTime设置为-1,这样才能开启看门狗机制。
在tryLockInnerAsync方法里尝试获取锁,如果获取锁成功调用scheduleExpirationRenewal执行看门狗机制
在scheduleExpirationRenewal中比较重要的方法就是renewExpiration,当线程第一次获取到锁(也就是不是重入的情况),那么就会调用renewExpiration方法开启看门狗机制。
在renewExpiration会为当前锁添加一个延迟任务task,这个延迟任务会在10s后执行,执行的任务就是将锁的有效期刷新为30s(这是看门狗机制的默认锁释放时间)
并且在任务最后还会继续递归调用renewExpiration。
也就是总的流程就是,首先获取到锁(这个锁30s后自动释放),然后对锁设置一个延迟任务(10s后执行),延迟任务给锁的释放时间刷新为30s,并且还为锁再设置一个相同的延迟任务(10s后执行),这样就达到了如果一直不释放锁(程序没有执行完)的话,看门狗机制会每10s将锁的自动释放时间刷新为30s。
而当程序出现异常,那么看门狗机制就不会继续递归调用renewExpiration,这样锁会在30s后自动释放。
或者,在程序主动释放锁后,流程如下:
将锁对应的线程ID移除
接着从锁中获取出延迟任务,将延迟任务取消
在将这把锁从EXPIRATION_RENEWAL_MAP中移除。
鸣谢