Redission分布式锁 watch dog 看门狗机制

news2024/12/24 8:35:03

为了避免Redis实现的分布式锁超时,Redisson中引入了watch dog的机制,他可以帮助我们在Redisson实例被关闭前,不断的延长锁的有效期。

  • 自动续租:当一个Redisson客户端实例获取到一个分布式锁时,如果没有指定锁的超时时间,Watchdog会基于Netty的时间轮启动一个后台任务,定期向Redis发送命令,重新设置锁的过期时间,通常是锁的租约时间的1/3。这确保了即使客户端处理时间较长,所持有的锁也不会过期。
  • 每次续期的时长:默认情况下,每10s钟做一次续期,续期时长是30s。
  • 停止续期:当锁被释放或者客户端实例被关闭时,Watchdog会自动停止对应锁的续租任务。

💖 底层实现

👨‍🏫 RedissonBaseLock.renewExpiration()

protected void scheduleExpirationRenewal(long threadId) {
     // 创建一个新的过期续期条目
     ExpirationEntry entry = new ExpirationEntry();
     // 尝试将新的过期续期条目放入到过期续期映射中,如果已存在则不替换
     ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
     if (oldEntry != null) {
         // 如果已存在,则将线程ID添加到旧的过期续期条目中
         oldEntry.addThreadId(threadId);
     } else {
         // 如果是新的过期续期条目,则添加线程ID,并尝试续期
         entry.addThreadId(threadId);
         try {
             // 尝试续期
             renewExpiration();
         } finally {
             // 如果当前线程被中断,则取消续期
             if (Thread.currentThread().isInterrupted()) {
                 cancelExpirationRenewal(threadId);
             }
         }
     }
}

// 定时任务执行续期
private void renewExpiration() {
     // 从过期续期映射中获取当前的过期续期条目
     ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
     if (ee == null) {
         // 如果没有找到,则直接返回
         return;
     }
     
     // 创建一个新的定时任务,用于执行续期逻辑
     Timeout task = getServiceManager().newTimeout(new TimerTask() {
         @Override
         public void run(Timeout timeout) throws Exception {
             // 再次检查过期续期条目是否仍然存在
             ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
             if (ent == null) {
                 return;
             }
             // 获取线程ID
             Long threadId = ent.getFirstThreadId();
             if (threadId == null) {
                 return;
             }
             
             // 使用LUA脚本异步续期
             CompletionStage<Boolean> future = renewExpirationAsync(threadId);
             future.whenComplete((res, e) -> {
                 if (e != null) {
                     // 如果有异常发生,记录错误并从映射中移除过期续期条目
                     log.error("Can't update lock {} expiration", getRawName(), e);
                     EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                     return;
                 }
                 
                 if (res) {
                     // 如果续期成功,则重新调度续期任务
                     renewExpiration();
                 } else {
                     // 如果续期失败,则取消续期
                     cancelExpirationRenewal(null);
                 }
             });
         }
     }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
     
     // 将定时任务与过期续期条目关联
     ee.setTimeout(task);
}

// 使用LUA脚本,进行续期
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
     // 使用evalWriteAsync方法异步执行LUA脚本,用于续期
     return evalWriteAsync(getRawName(), 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(getRawName()),
             internalLockLeaseTime, getLockName(threadId));
}

可以看到,上面的代码的主要逻辑就是用了一个TimerTask来实现了一个定时任务,设置了internalLockLeaseTime / 3的时长进行一次锁续期。默认的超时时长是30s,那么他会每10s进行一次续期,通过LUA脚本进行续期,再续30s

不过,这个续期也不是无脑续,他也是有条件的,其中ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());这个值得我们关注,他会从EXPIRATION_RENEWAL_MAP中尝试获取一个KV对,如果查不到,就不续期了。

EXPIRATION_RENEWAL_MAP这个东西,会在unlock的时候操作的,对他进行remove,所以一个锁如果被解了,那么就不会再继续续期了

@Override
public void unlock() {
     try {
         // 异步执行解锁操作
         get(unlockAsync(Thread.currentThread().getId()));
     } catch (RedisException e) {
         // 检查异常是否是由于线程没有持有锁导致的
         if (e.getCause() instanceof IllegalMonitorStateException) {
             // 如果是,则抛出原始的 IllegalMonitorStateException异常
             throw (IllegalMonitorStateException) e.getCause();
         } else {
             // 如果不是,则抛出原始的RedisException异常
             throw e;
         }
     }
}

@Override
public RFuture<Void> unlockAsync(long threadId) {
     // 使用getServiceManager执行解锁操作
     return getServiceManager().execute(() -> unlockAsync0(threadId));
}

private RFuture<Void> unlockAsync0(long threadId) {
     // 异步执行解锁操作
     CompletionStage<Boolean> future = unlockInnerAsync(threadId);
     // 处理异步操作的结果或异常
     CompletionStage<Void> f = future.handle((opStatus, e) -> {
         // 取消续期任务
         cancelExpirationRenewal(threadId);

         if (e != null) {
             // 如果有异常发生,抛出CompletionException
             if (e instanceof CompletionException) {
                 throw (CompletionException) e;
             }
             throw new CompletionException(e);
         }
         if (opStatus == null) {
             // 如果解锁操作失败,抛出IllegalMonitorStateException
             IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                     + id + " thread-id: " + threadId);
             throw new CompletionException(cause);
         }

         return null;
     });

     // 将CompletableFuture包装为RFuture
     return new CompletableFutureWrapper<>(f);
}

protected void cancelExpirationRenewal(Long threadId) {
     // 从过期续期映射中获取过期续期条目
     ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
     if (task == null) {
         // 如果没有找到,则直接返回
         return;
     }
     
     if (threadId != null) {
         // 如果线程ID不为空,则从过期续期条目中移除该线程ID
         task.removeThreadId(threadId);
     }

     if (threadId == null || task.hasNoThreads()) {
         // 如果线程ID为空或者过期续期条目中没有线程ID,则取消定时任务
         Timeout timeout = task.getTimeout();
         if (timeout != null) {
             timeout.cancel();
         }
         // 从过期续期映射中移除过期续期条目
         EXPIRATION_RENEWAL_MAP.remove(getEntryName()); // 取消续期关键点
     }
}

核心:EXPIRATION_RENEWAL_MAP.remove(getEntryName());

一次unlock过程中,对EXPIRATION_RENEWAL_MAP进行移除,进而取消下一次锁续期的实现细节。

并且在unlockAsync方法中,不管unlockInnerAsync是否执行成功,还是抛了异常,都不影响cancelExpirationRenewal的执行,也可以理解为,只要unlock方法被调用了,即使解锁未成功,那么也可以停止下一次的锁续期。

💖 续期

加锁代码

/**
 * 尝试异步获取分布式锁。
 *
 * @param waitTime      等待获取锁的最大时间,如果设置为-1,则表示无限等待。
 * @param leaseTime     锁的过期时间,如果设置为-1,则表示使用默认的过期时间。
 * @param unit          时间单位,用于将leaseTime转换为毫秒。
 * @param threadId      当前线程的唯一标识符。
 * @return              一个RFuture对象,表示异步操作的结果,如果成功获取锁,则返回剩余的过期时间(毫秒)。
 * @throws InterruptedException 如果当前线程在等待过程中被中断。
 */
private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
     // 尝试获取锁的异步方法
     RFuture<Long> ttlRemainingFuture;
     // 如果锁的过期时间大于0,则使用指定的过期时间
     if (leaseTime > 0) {
         ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
     } else {
         // 如果锁的过期时间不大于0,则使用内部锁的过期时间
         ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                 TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
     }
     // 处理没有同步获取锁的情况
     CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);
     // 将处理后的CompletionStage包装为RFuture
     ttlRemainingFuture = new CompletableFutureWrapper<>(s);

     // 当ttlRemainingFuture完成时,如果ttlRemaining为null,则表示锁已成功获取
     CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
         // 锁已获取
         if (ttlRemaining == null) {
             // 如果锁的过期时间大于0,则设置锁的过期时间
             if (leaseTime > 0) {
                 internalLockLeaseTime = unit.toMillis(leaseTime);
             } else {
                 // 如果锁的过期时间不大于0,则安排锁的过期时间续期
                 scheduleExpirationRenewal(threadId);
             }
         }
         // 返回ttlRemaining,如果为null,则表示锁已获取
         return ttlRemaining;
     });
     // 将处理后的CompletionStage包装为RFuture
     return new CompletableFutureWrapper<>(f);
}

在这里插入图片描述

💖 停止续期

如果一个锁的unlock方法被调用了,那么就会停止续期。

那么,取消续期的核心代码如下:

/**
 * 取消与锁关联的自动续期任务。
 *
 * @param threadId 如果不为null,则只取消与特定线程ID关联的续期任务。
 */
protected void cancelExpirationRenewal(Long threadId) {
     // 从过期续期映射中获取当前的过期续期条目
     ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
     if (task == null) {
         // 如果没有找到对应的续期条目,则直接返回
         return;
     }
     
     if (threadId != null) {
         // 如果提供了线程ID,则从续期条目中移除该线程ID
         task.removeThreadId(threadId);
     }

     if (threadId == null || task.hasNoThreads()) {
         // 如果没有提供线程ID,或者续期条目中没有其他线程ID,则取消定时任务
         Timeout timeout = task.getTimeout();
         if (timeout != null) {
             // 取消定时任务
             timeout.cancel();
         }
         // 从过期续期映射中移除过期续期条目
         EXPIRATION_RENEWAL_MAP.remove(getEntryName());
     }
}

主要就是通过 EXPIRATION_RENEWAL_MAP.remove来做的。那么cancelExpirationRenewal还有下面一处调用:

protected void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        try {
            renewExpiration();
        } finally {
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}

也就是说,在尝试开启续期的过程中,如果线程被中断了,那么就会取消续期动作了。

目前,Redisson是没有针对最大续期次数和最大续期时间的支持的。所以,正常情况下,如果没有解锁,是会一直续期下去的。


💖 客户端挂了,锁会不会一直续期?

Redission 是 redis 的客户端

在这里插入图片描述

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

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

相关文章

cookie没有携带的问题

背景&#xff1a; build-model应用在hcs迁移的时候&#xff0c;前、后端各自部署了一个新应用&#xff0c;但是调试时候发现没有cookie&#xff0c;导致鉴权失败&#xff01; 注&#xff1a; 后端通过cookie中的token做鉴权的&#xff0c;前端调用接口的时候&#xff0c;查看&…

我是如何带团队从0到1做了AI中台

经历心得 我从18年初就开始带这小团队开始做项目&#xff0c;比如最初的数字广东的协同办公项目&#xff0c;以及粤信签小程序等&#xff0c;所以&#xff0c;在团队管理&#xff0c;人员安排&#xff0c;工作分工&#xff0c;项目拆解等方面都有一定的经验。 19年中旬&#…

微搭低代码入门03页面管理

目录 1 创建页面2 页面布局3 页面跳转总结 上一篇我们介绍了应用的基本操作&#xff0c;掌握了应用的概念后接着我们需要掌握页面的常见操作。 1 创建页面 打开应用的编辑器&#xff0c;在顶部导航条点击创建页面图标 在创建页面的时候可以从空白新建&#xff0c;也可以使用模…

第78天:WAF攻防-菜刀冰蝎哥斯拉流量通讯特征绕过检测反制感知

目录 案例一&#xff1a; 菜刀-流量&绕过&特征&检测 菜刀的流量特征 案例二&#xff1a;冰蝎-流量&绕过&特征&检测 冰蝎使用教程 冰蝎的流量特征 案例三&#xff1a; 哥斯拉-流量&绕过&特征&检测 哥斯拉使用教程 哥斯拉的流量特征…

龙迅LT9211D MIPI桥接到2 PORT LVDS,分辨率支持高达3840*2160*30HZ

龙迅LT9211D描述&#xff1a; Lontium LT9211D是一款高性能的MIPI DSI/CSI- 2到双端口LVDS转换器。LT9211D反序列化输入的MIPI视频数据&#xff0c;解码数据包&#xff0c;并将格式化的视频数据流转换为AP和移动显示面板或摄像机之间的LVDS发射机输出。LT9211D支持最大14 dB输…

基于Springboot+Vue+Java的学生就业管理系统

&#x1f49e; 文末获取源码联系 &#x1f649; &#x1f447;&#x1f3fb; 精选专栏推荐收藏订阅 &#x1f447;&#x1f3fb; &#x1f380;《Java 精选实战项目-计算机毕业设计题目推荐-期末大作业》&#x1f618; 更多实战项目~ https://www.yuque.com/liuyixin-rotwn/ei3…

MYSQL基础架构、执行过程分析、事务的实现、索引的选择、覆盖索引

本文是mysql45讲的1-5的总结 文章目录 基础架构连接器分析器优化器执行器SQL查询执行过程详细执行步骤 SQL更新执行过程重要的日志模块&#xff1a;redo log重要的日志模块&#xff1a;binlog阶段性提交 事务事务隔离的实现启动 索引数据库索引模型InnoDB索引组织结构主键选择…

电源小白入门学习7——USB充电、供电、电源路径管理

电源小白入门学习7——USB充电、供电、电源路径管理 USB充电系统需要考虑的因素开关充电和线性充电充电路径管理输入限流路径管理&#xff08;动态功率管理&#xff09;理想二极管帮助提高电池利用率输入过充抑制 上期我们介绍了锂离子电池的电池特性&#xff0c;及充电电路设计…

字节跳动(社招)三面算法原题

TikTok 喘息 继上月通过强制剥离 TikTok 法案后&#xff0c;美国众议院在当地时间 20 日下午以 360 票赞成 58 票反对通过了新的法案&#xff1a;剥离 TikTok 的期限由生效后 165 天调整至 270 天之内&#xff0c;即今年 11 月的美国总统大选后。 之前我们讲过&#xff0c;TikT…

[安全开发]如何搭建一款自己的网安微信机器人

前言 hxd写的一个微信网安机器人。 原理 基于HOOK的微信机器人&#xff0c;以往的机器人大多数为协议机器人&#xff0c;封号概率极大&#xff08;下面会详细讲解hook和协议的区别&#xff09;&#xff0c;而HOOK机制的大大减小了封号几率。 什么是协议机器人&#xff1f; …

腾讯云服务器产品特惠集合

腾讯云服务器近期推出了多项特惠活动&#xff0c;以满足不同用户的需求。以下是一些主要的特惠信息&#xff1a; 特惠产品合集页 精选特惠 用云无忧 腾讯云提供了极具竞争力的价格。例如&#xff0c;用户可以找到2核2G3M配置的云服务器&#xff0c;月费低至5.08元&#xff1b;…

【C++之map的应用】

C学习笔记---021 C之map的应用1、map的简单介绍1.1、基本概念1.2、map基本特性 2、map的基本操作2.1、插入元素2.2、访问元素2.3、删除元素2.4、遍历map2.5、检查元素是否存在2.6、获取map的大小2.7、清空map2.8、基本样例 3、map的基础模拟实现4、测试用例4.1、插入和遍历4.2、…

Python查询PostgreSQL数据库

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; Python与PostgreSQL的连接 需要了解如何在Python中连接到PostgreSQL数据库。这通常涉及到使用一个库&#xff0c;如psycopg2&#xff0c;它是Python中用于PostgreSQL的最流行的适配器。安装psycopg2非常简单&#x…

[Java EE] 多线程(八):CAS问题与JUC包

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏:&#x1f355; Collection与数据结构 (90平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm1001.2014.3001.5482 &#x1f9c0;Java …

用 Go map 要注意这个细节,避免依赖他!

有的小伙伴没留意过 Go map 输出、遍历顺序&#xff0c;以为它是稳定的有序的&#xff0c;会在业务程序中直接依赖这个结果集顺序&#xff0c;结果栽了个大跟头&#xff0c;吃了线上 BUG。 有的小伙伴知道是无序的&#xff0c;但却不知道为什么,有的却理解错误&#xff1f; 今…

Flutter笔记:Widgets Easier组件库 - 使用标签(Tag)

Flutter笔记 Widgets Easier组件库 - 使用标签&#xff08;Tag&#xff09; - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this …

滑动窗口 | 1652. 拆炸弹 |LeetCode

文章目录 题目介绍暴力(可以过力扣竟然。不愧是简单题)&#xff1a;滑动窗口 祝你天天开心 题目介绍 你有一个炸弹需要拆除&#xff0c;时间紧迫&#xff01;你的情报员会给你一个长度为 n 的 循环 数组 code 以及一个密钥 k 。 为了获得正确的密码&#xff0c;你需要替换掉每…

【嵌入式笔试题】进程线程笔试题

非常经典的笔试题。 1.进程&线程(16道) 1.1异步IO和同步IO区别? 答案:如果是同步IO,当一个IO操作执行时,应用程序必须等待,直到此IO执行完。 相反,异步IO操作在后台运行,IO操作和应用程序可以同时运行,提高系统性能,提 高IO流量。 解读:在同步文件IO中,线…

IntelliJ IDEA 2024 for Mac:Java开发者的强大助手

IntelliJ IDEA 2024 for Mac是Java开发者不可或缺的强大助手&#xff0c;它凭借卓越的性能和丰富的功能&#xff0c;赢得了广大开发者的青睐。 作为集成开发环境&#xff08;IDE&#xff09;的佼佼者&#xff0c;IDEA 2024提供了全面的代码编辑和智能提示功能。它不仅能实时分析…

视频素材哪个软件好用?8个短视频素材高清无水印

在今日这个视觉表现至关重要的时代&#xff0c;获取合适的视频素材成为制作任何类型视频内容的基石。从企业宣传片到社交媒体短视频&#xff0c;高质量的视频素材能够显著提升内容的吸引力和专业度。这里列出了一些全球顶尖的视频素材平台&#xff0c;每一个都能为您的视频项目…