水煮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对象。
相关推荐
测开小菜鸟5 分钟前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity1 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天1 小时前
java的threadlocal为何内存泄漏
java
caridle1 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^1 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋31 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花2 小时前
【JAVA基础】Java集合基础
java·开发语言·windows
小松学前端2 小时前
第六章 7.0 LinkList
java·开发语言·网络
Wx-bishekaifayuan2 小时前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源