开源项目前导 剖析Redisson源码tryLock() 看门狗续期(2)

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中移除。

鸣谢

【Redis进阶】一文搞懂Redisson的看门狗机制底层实现-CSDN博客

  • 8
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值