水煮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对象。
相关推荐
寻星探路2 小时前
【深度长文】万字攻克网络原理:从 HTTP 报文解构到 HTTPS 终极加密逻辑
java·开发语言·网络·python·http·ai·https
陌上丨5 小时前
Redis的Key和Value的设计原则有哪些?
数据库·redis·缓存
曹牧5 小时前
Spring Boot:如何测试Java Controller中的POST请求?
java·开发语言
爬山算法5 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
kfyty7256 小时前
集成 spring-ai 2.x 实践中遇到的一些问题及解决方案
java·人工智能·spring-ai
猫头虎6 小时前
如何排查并解决项目启动时报错Error encountered while processing: java.io.IOException: closed 的问题
java·开发语言·jvm·spring boot·python·开源·maven
李少兄6 小时前
在 IntelliJ IDEA 中修改 Git 远程仓库地址
java·git·intellij-idea
忆~遂愿6 小时前
ops-cv 算子库深度解析:面向视觉任务的硬件优化与数据布局(NCHW/NHWC)策略
java·大数据·linux·人工智能
小韩学长yyds6 小时前
Java序列化避坑指南:明确这4种场景,再也不盲目实现Serializable
java·序列化