水煮Redisson(十九)-看门狗的最终用途

前言

看门狗的定义,在之前一个小章节中已经有简单说明,这里不做赘述。但是之前只是说了其与持有时间,等待时间的区别,没有详细介绍,下面我们来进行深入分析,解开迷雾。

设置看门狗时间:org.redisson.config.Config.setLockWatchdogTimeout(30 * 1000);

看门狗作业流程

假设看门狗时间为30秒【默认】,如果线程没有设置超时时间,那么设置internalLockLeaseTime时间为看门狗时间。在线程持有锁之后,每隔1/3 * internalLockLeaseTime,会对锁进行续命,每次续命的时间为internalLockLeaseTime.

RedissonLock中有个静态内部类,ExpirationEntry,一个锁资源对应一个entry实例,保存在一个静态Map集合中【EXPIRATION_RENEWAL_MAP】,entry里面存储了当前锁资源里持有锁的所有线程id集合【threadIds】,定时任务会遍历此线程id集合,对其持有的资源进行续命。
问题:一个资源只有一个ExpirationEntry实例,为什么会有一个线程集合?

原因其实可以想到,之前讲解过了Redisson的读写锁,其中读锁是共享锁类型,会有多个线程id持有同一个锁资源。

如何生效

如果没有设置持有时间,那么表示锁是不会超时的,这时就用到看门狗了

scss 复制代码
    private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime, TimeUnit unit, long threadId) {
        if (leaseTime != -1) {
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        }
		// 没有设定持有时间,将其设置为看门狗时间
        RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }
            // 获取锁成功之后,开启定时任务
            if (ttlRemaining) {
                scheduleExpirationRenewal(threadId);
            }
        });
        return ttlRemainingFuture;
    }

执行步骤:

  1. 判断是否设定了持有时间,如果没有,则将持有时间设定为看门狗时间;
  2. 持有成功之后,开启定时任务,参数为当前线程id;

重入设定

持有锁成功之后,判断此线程id是否已经保存在当前jvm进程中【scheduleExpirationRenewal】

ini 复制代码
    private void scheduleExpirationRenewal(long threadId) {
        ExpirationEntry entry = new ExpirationEntry();
		// entryName是当前锁资源的描述,entryName = id + ":" + name;
        ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
        if (oldEntry != null) {
            oldEntry.addThreadId(threadId);
        } else {
            entry.addThreadId(threadId);
            renewExpiration();
        }
    }

执行步骤:

  1. 在EXPIRATION_RENEWAL_MAP中,判断当前资源是否已经存在,如果存在,表示已经开启了定时任务,以免定时任务重复。然后将线程id添加到ExpirationEntry的threadIds集合中,定时任务会遍历执行;
  2. 如果资源不存在,则添加线程id之后,直接开启;

如何续命

遍历集合中的线程id,执行续命操作【renewExpiration】

关键代码

ini 复制代码
    private void renewExpiration() {
        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) {
                        // reschedule itself
                        renewExpiration();
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        // 将定时任务设置到entry实例中,timeout属性在entry对象中,是volatile的,可见其考虑到了解锁时,是否能及时拿到此task的可能性。
        ee.setTimeout(task);
    }

执行步骤:

  1. 判断Map集合中,是否存在锁资源,避免其他线程解锁时,在集合中删除此实例;
  2. 如果存在,则开启定时任务;
  3. 定时任务中,再次判断锁资源是否为空和线程id是否为空,如果为空,则结束定时任务;
  4. 执行成功后,开启下一次定时任务,定时任务周期为1/3看门狗时间;

手动续命

根据参数里设定的时间【internalLockLeaseTime】来重置锁过期时间

typescript 复制代码
    protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return 1; " +
                        "end; " +
                        "return 0;",
                Collections.singletonList(getName()),
                internalLockLeaseTime, getLockName(threadId));
    }

执行步骤:

  1. 判断锁资源是否存在,如果存在,则重置锁过期时间,时间为看门狗时间;
  2. 如果不存在,直接返回false;

取消续命任务

当解锁成功后【重入锁需要将持有次数减为零】,取消续命定时任务;

ini 复制代码
    void cancelExpirationRenewal(Long threadId) {
        ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (task == null) {
            return;
        }
        
        if (threadId != null) {
            task.removeThreadId(threadId);
        }

        if (threadId == null || task.hasNoThreads()) {
            Timeout timeout = task.getTimeout();
            if (timeout != null) {
                timeout.cancel();
            }
            EXPIRATION_RENEWAL_MAP.remove(getEntryName());
        }
    }

执行条件:当解锁成功后【重入锁需要将持有次数减为零】,取消续命定时任务;

执行步骤:

  1. 判断entry对象是否为空,为空直接返回,表示没有续命定时任务【如果拿锁时,设定了持有时间,就不会有续命定时任务】;
  2. 取出entry对象中的定时任务,如果不为空则取消;
  3. 删除EXPIRATION_RENEWAL_MAP集合中的entry对象。
相关推荐
咩咩啃树皮6 分钟前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚40 分钟前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白2 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司3 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地4 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun3141594 小时前
TCP超时重传机制是为了解决什么问题?
java
二宝哥5 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
莫逸风6 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
z123456789867 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
yaoxin5211237 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python