Redisson的看门狗机制究竟有什么用?

news2025/1/12 18:38:41

Redisson的看门狗机制究竟有什么用?

一、普通的Redis分布式锁的缺陷

基于Redis的分布式锁

Redis + Lua 脚本实现分布式锁

二、watchDog的自动延期机制

调用链关系

源码解析 

tryLock() 

tryAcquire() 

tryAcquireAsync() 

scheduleExpirationRenewal()  锁续约

renewExpiration()  更新有效期

Lua脚本 重置有效期

更新有效期 - renewExpiration() 执行流程

总结


Redisson的看门狗机制究竟有什么用?

想要讨论这个问题,就要先搞清楚Redisson是干什么的?解决了什么问题?

8. 分布式锁和同步器 - 《Redisson 使用手册》 - 书栈网 · BookStackicon-default.png?t=M85Bhttps://www.bookstack.cn/read/redisson-wiki-zh/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8.mdRedisson是一种分布式锁的解决方案!它为我们封装了很多API,使程序员在开发的时候只要关注业务逻辑,不需要考虑锁可靠性的问题。

分布式锁要是设计不当很可能发生死锁的问题,而Redisson中的看门狗机制就是为了解决这个问题!

一、普通的Redis分布式锁的缺陷

我之前写过两篇文章,就是关于自己手写一个redis分布式锁解决高并发下超卖问题的

基于Redis的分布式锁

Redis的分布式锁问题(八)基于Redis的分布式锁_面向鸿蒙编程的博客-CSDN博客_redis分布式锁icon-default.png?t=M85Bhttps://blog.csdn.net/weixin_43715214/article/details/127967364

Redis + Lua 脚本实现分布式锁

Redis的分布式锁问题(九)Redis + Lua 脚本实现分布式锁_面向鸿蒙编程的博客-CSDN博客icon-default.png?t=M85Bhttps://blog.csdn.net/weixin_43715214/article/details/127982757虽然,最后这种基于Lua脚本实现的分布式锁基本上已经可以应对市面上大多数场景,但是,它还存在很多问题——可重入问题、可重试问题、超时释放问题、主从一致性问题......

还有一个很重要的问题就是分布式锁过期的问题!!!

试想一下,一个锁设置了1分钟超时释放,如果拿到这个锁的线程在一分钟内没有执行完毕,那么这个锁就会被其他线程拿到,可能会导致严重的线上问题!

所以Redisson中就提供了这种看门狗机制来解决这个问题!

二、watchDog的自动延期机制

Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。 

默认情况下,看门狗的续期时间是30s,也可以通过修改 Config.lockWatchdogTimeout 来另行指定。另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间,但是一旦使用了这个参数看门狗机制就失效了(不会自动续期)

调用链关系

tryLock()->tryAcquire()->tryAcquireAsync() -> scheduleExpirationRenewal()

源码解析 

tryLock() 

public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
    return tryLock(waitTime, -1, unit);
}
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    // 转成毫秒,后面都是以毫秒为单位
    long time = unit.toMillis(waitTime);
    // 当前时间
    long current = System.currentTimeMillis();
    // 线程ID-线程标识
    long threadId = Thread.currentThread().getId();
 
    // 尝试获取锁 tryAcquire() !!!
    Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
        
    // 如果上面尝试获取锁返回的是null,表示成功;如果返回的是时间则表示失败。
    if (ttl == null) {
        return true;
    }
 
    // 剩余等待时间 = 最大等待时间 -(用现在时间 - 获取锁前的时间)
    time -= System.currentTimeMillis() - current;
 
    // 剩余等待时间 < 0 失败
    if (time <= 0) {
        acquireFailed(waitTime, unit, threadId);
        return false;
    }
 
    // 再次获取当前时间
    current = System.currentTimeMillis();
    // 重试逻辑,但不是简单的直接重试!
    // subscribe是订阅的意思
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    // 如果在剩余等待时间内,收到了释放锁那边发过来的publish,则才会再次尝试获取锁
    if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
        if (!subscribeFuture.cancel(false)) {
            subscribeFuture.onComplete((res, e) -> {
                if (e == null) {   
                    // 取消订阅
                    unsubscribe(subscribeFuture, threadId);
                }
            });
        }
        // 获取锁失败
        acquireFailed(waitTime, unit, threadId);
        return false;
    }
 
    try {
        // 又重新计算了一下,上述的等待时间
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
 
        // 重试!
        while (true) {
            long currentTime = System.currentTimeMillis();
            ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
 
            // 成功
            if (ttl == null) {
                return true;
            }
            
            // 又获取锁失败,再次计算上面的耗时
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
 
            currentTime = System.currentTimeMillis();
            // 采用信号量的方式重试!
            if (ttl >= 0 && ttl < time) {
                subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
            }
            
            // 重新计算时间(充足就继续循环)
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        }
    } finally {
        unsubscribe(subscribeFuture, threadId);
    }
}

tryAcquire() 

private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}

tryAcquireAsync() 

这个就是看门狗机制的入口,当没有传leaseTime时,会触发看门狗机制

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
 
    // leaseTime我们没有传,这里设定默认值(看门狗)30s
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
            commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
            TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
 
    // 回调函数 ttlRemaining:剩余有效期,e:异常
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }
 
        // 剩余有效期为null,表示获取锁成功!
        if (ttlRemaining == null) {
            // 锁续约
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

scheduleExpirationRenewal()  锁续约

private void scheduleExpirationRenewal(long threadId) {
    RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
 
    /* putIfAbsent() 是ConcurrentHashMap的API
     * (1)如果是新的记录,那么会向map中添加该键值对,并返回null
     * (2)如果已经存在,那么不会覆盖已有的值,直接返回已经存在的值
     * EXPIRATION_RENEWAL_MAP 是静态的,key为锁的名称
     */
    RedissonLock.ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
 
    // 新的、旧的都会加
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        // 新的,还会多一步操作(更新有效期)
        renewExpiration();
    }
}

renewExpiration()  更新有效期

这个方法通过自己调用自己的方式去实现锁续期! 每10s执行一次

private void renewExpiration() {
    RedissonLock.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 {
            RedissonLock.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) {
                    // 自己调自己
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
 
    ee.setTimeout(task);
}

Lua脚本 重置有效期

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));
}

更新有效期 - renewExpiration() 执行流程

 renewExpiration() 函数在执行时,会开启一个任务

这个任务会在10s后执行 (internalLockLeaseTime / 3)

10s后执行的这个任务会更新有效期,并“调自己”!

“调自己”说明又会建立这个任务,而这个任务又在10s后执行.......

这也是为什么在oldEntry中不会调这个函数,而新的entry需要调的原因!!! 

因为在oldEntry中本身就有这个任务(之前调过,当它刚刚成为entry的时候) ,最后,在释放锁的时候将这个定时任务清除(通过cancelExpirationRenewal()清除任务)

总结

(1)watchDog 在当前节点存活时,每10s给分布式锁的key续期 30s

(2)watchDog 最终还是通过 Lua脚本expire命令来进行重置有效期,更新有效期

(3)watchDog 机制启动,且代码中没有释放锁操作时,watchDog 会不断的给锁续期

(4)要使 watchDog 机制生效 ,就不要设置锁过期时间 leaseTime

(5)锁续期是通过一个定时任务,在 renewExpiration() 中自己调自己实现的!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/70689.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Nginx+tomcat集群

Nginxtomcat集群 定义&#xff1a;在测试环境中有多个tomcat运行这一个项目&#xff0c;需要nginx管理 2特点&#xff1a;1&#xff0c;增加项目访问能力 2&#xff0c;增加服务器性能 3&#xff0c;实现不同负载均衡 3负载均衡&#xff1a;根据每台服务器的性能分配其能承受住…

【Linux】程序地址空间

目录 地址空间是什么 写时拷贝 地址空间存在的意义 如何管理进程地址空间 地址空间是什么 这是我们常说的c/c程序地址空间&#xff0c;但是这里的空间具体指的是哪里的空间&#xff0c;举一个例子方便理解&#xff1a; 运行结果&#xff1a; 代码运行十秒后子进程修改了全局…

目标检测—DiffusionDet:用于检测的概率扩散模型笔记—DDPM

目录 一、Introduction 二、相关工作 三、方法 1.准备工作 2.架构 3、训练 4.预测过程 四. 实验 1.训练策略 2.main property 3.消融实验 五、代码分析 1.测试 demo.py 2.训练 train-net.py 总结 DiffusionDet&#xff1a;将目标检测问题构建为一个从带噪框到目…

安卓APP源码和设计报告——麻雀笔记

目录 一 安卓应用程序开发背景3 1.1开发背景3 1.2开发环境4 二 安卓应用程序开发理论与方法4 三 记事本应用程序的设计与实现5 3.1 拟解决的问题及目标5 3.2 总体设计6 3.3 详细设计与编码实现6 四 总结23 一 安卓应用程序开发背景 1.1开发背景 1.智能手机的市场 …

四年一次的世界杯来了,看看还剩下哪些赛程呢?

足球世界杯的由来2022年的世界杯目前世界杯的赛程看看排在前十的球员几个疑问&#xff1f;世界杯的由来 足球大家应该知道&#xff0c;是一项体育运动&#xff0c;而且是一项多人的体育运动。 世界杯的诞生&#xff0c;源于1950年的南非公开赛&#xff0c;在当时只有5支队伍参…

微信视频播放点播小程序毕业设计,在线教育视频学习小程序系统设计与实现,微信小程序毕业设计论文怎么写毕设源码开题报告需求分析怎么做

基于微信小程序的毕业设计题目&#xff08;12&#xff09;php在线教育视频点播学习小程序(含开题报告、任务书、中期报告、答辩PPT、论文模板) 项目背景和意义 目的&#xff1a;本课题主要目标是设计并能够实现一个基于微信小程序视频点播系统&#xff0c;前台用户使用小程序&a…

理解Nodejs中的进程间通信

前置知识 文件描述符 在 Linux 系统中&#xff0c;一切都看成文件&#xff0c;当进程打开现有文件时&#xff0c;会返回一个文件描述符。 文件描述符是操作系统为了管理已经被进程打开的文件所创建的索引&#xff0c;用来指向被打开的文件。 当我们的进程启动之后&#xff0c…

Python学习-8.1.2 标准库(random库的基础与实例)

2.2 random库 使用random库的主要目的是生成随机数 2.2.1 产生随机数 random()函数&#xff1a;生成一个[0.0,1.0)之间的随机小数&#xff0c;左开右闭。 import random print(random.random())#生成一个[0.0,1.0)之间的随机小数 #每次运行random()函数都会产生不同的数据 …

Layer2代币经济学:除了治理 还应该具备什么价值?

为什么我们需要L2&#xff1f; 任何在2021年期间使用以太坊的人都知道&#xff0c;该区块链可能会变得非常拥堵。这是一个典型的问题——需求太多而供应不足。因此&#xff0c;gas费用&#xff08;交易费&#xff09;变得相当昂贵。在牛市高峰期&#xff0c;使用以太坊区块链发…

基于Jenkins的开发测试全流程持续集成实践

今年一直在公司实践CI&#xff0c;本文将近半年来的一些实践总结一下&#xff0c;可能不太完善或优美&#xff0c;但的确初步解决了我目前所在项目组的一些痛点。当然这仅是一家之言也不够完整&#xff0c;后续还会深入实践和引入Kubernetes进行容器编排&#xff0c;以及通过阿…

从js中加载图片和Cannot read property ‘appendChild‘ of null 错误

先写一段代码如下&#xff1b; <!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title> </title><script>var imgnew Image();img.addEventListener("load",loadHandler);img.src"img/1.jpg"…

【JMeter】命令参数说明

jmeter -n -t xxx.jmx -l result.jtl 非GUI运行xxx.jml脚本写入xxx.jtl结果文件 jmeter -n -t xxx.jmx -l result.jtl -j run.log 非GUI运行xxx.jmx脚本写入xxx.jtl结果文件&#xff0c;日志记录到run.log jmeter -n -t xxx.jmx -R ip1:port1,ip2:port2 -l result.jtl 使用远…

详解 Redis 持久化之掌握 RDB ⽂件的格式,学习如何制作数据库镜像

本文带大家了解一下 Redis 数据一种持久化方式 RDB 的实现。包括 Redis 内存快照 RDB ⽂件的创建时机以及⽣成⽅法。可以让你掌握 RDB ⽂件的格式&#xff0c;学习如何制作数据库镜像。 RDB 创建的入口函数 Redis 创建 RDB 文件的函数有三个&#xff0c;分别是 rdbSave, rdbSa…

知识图谱-KGE-语义匹配-双线性模型-2017:ANALOGY

【paper】 Analogical Inference for Multi-relational Embeddings【简介】 本文是卡耐基梅隆大学的中国学者发表在 ICML 2017 上的工作&#xff0c;提出了 ANALOGY 模型&#xff0c;用于建模实体和关系的推理属性。这个模型应当也算是双线性模型中比较经典的一个了&#xff0c…

Erueka基本使用

SpringCloud Erueka基本使用 Erueka是微服务架构中&#xff0c;可以作为注册中心的技术实现&#xff0c;如下图所示 服务提供者&#xff1a;一次业务中&#xff0c;暴露接口给其它微服务调用&#xff0c;被其它微服务调用的服务。&#xff08;提供接口给其它微服务&#xff09…

一个超好看的音乐网站设计与实现(HTML+CSS)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

vue+vite的创建

1、创建vue3项目 yarn create vite效果&#xff1a; yarn create v1.22.19 [1/4] Resolving packages... [2/4] Fetching packages... [3/4] Linking dependencies... [4/4] Building fresh packages...success Installed "create-vite3.2.1" with binaries:- crea…

接口管理测试繁琐复杂?何不试试这个神器

一、前言 作为一名测试从业者&#xff0c;深刻的明白接口测试在项目过程中是多么重要的一个环节。通过页面进行的UI测试会因为界面不稳定而导致用例维护非常困难。另外&#xff0c;在检查系统的安全性、稳定性上面也是尤为重要的环节&#xff0c;这些也是无法通过前端测试的&a…

Redis - Windows下载与安装

1.获取Redis在windows下的安装包 Windows版下载地址&#xff1a;https://github.com/microsoftarchive/redis/releases 选择Redis-x64-*.zip 2.解压zip文件与配置 2.1 选取目录 选取一个目录作为解压目录&#xff0c;这个目录就是你Redis程序所在位置&#xff0c;尽量找一…

【软件测试】师傅给我的测试新手“真理“宝典......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 熟悉你所测试的软件…