【Redisson源码】可重入锁看门狗机制

news2024/11/18 13:44:51

【本篇文章基于redisson-3.17.6版本源码进行分析】

为什么需要自动续期?

设想一下,如果我们指定的锁的超时时间是30秒,但是业务执行在30秒内还没有执行完成,此时分布式锁超时过期自动释放,其它线程就能获取到这把锁,这样就有问题了。

为了保证业务执行完后,锁才能够释放,Redisson提供了看门狗(watch dog)机制,实现了锁的自动续期。

本文就一起看看加锁成功之后的看门狗是如何实现的?

我们再回来看Redisson中异步加锁方法tryAcquireAsync():

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Long> ttlRemainingFuture;
    if (leaseTime > 0) {
        // 指定了超时时间
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        // 未指定超时时间   internalLockLeaseTime默认就是看门狗的超时时间:30秒
        // waitTime: -1
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
        // lock acquired

        // ttlRemaining为null, 其实就是加锁的LUA脚本中返回的nil,表示获取锁成功
        if (ttlRemaining == null) {
            if (leaseTime > 0) {   // 如果设置了超时时间,则更新internalLockLeaseTime为指定的超时时间,并且不会启动看门狗
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                // 自动续期实现,看门狗机制入口

                /**
                 * 1. 只有用户未指定锁的超时时间,看门狗才生效;如果我们指定了锁超时时间,则看门狗不会启动;
                 * 2. 获取锁成功的线程才启动看门狗
                 * 例:
                 * lock.lock(); 开启看门狗
                 * lock.lock(5000, TimeUnit.SECONDS); 不开启看门狗
                 */
                scheduleExpirationRenewal(threadId);
            }
        }
        return ttlRemaining;
    });
    return new CompletableFutureWrapper<>(f);
}

通过前面文章的分析,我们知道,当加锁的LUA脚本返回nil时,表示的就是加锁成功了。

对应到上面的代码就是tryLockInnerAsync()方法的返回值是null:

从源码中可以看到,并不是所有情况下,Redisson都会启动看门狗自动续期的。

只有同时满足下面两个条件,Redisson才会启动看门狗机制:

  • 1、当前线程获取锁成功;
  • 2、未指定锁的超时时间,看门狗才生效;如果我们指定了锁超时时间,则看门狗不会启动;

接下来我们进入scheduleExpirationRenewal(threadId)方法,看名字是到期续订的意思。

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

进入renewExpiration(),看门狗的重要逻辑都在这个方法:

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    // 缓存为空,则不需要自动续期,直接返回
    if (ee == null) {
        return;
    }

    /**
     * 基于Netty时间轮实现自动续期,实际上就是一个延迟定时任务
     *  【internalLockLeaseTime / 3】: 第一次续期是在【过期时间 / 3】时执行,默认是每10秒将锁的过期时间续期为30秒
     */
    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;
            }

            // 执行自动续期的核心代码
            CompletionStage<Boolean> future = renewExpirationAsync(threadId);
            future.whenComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getRawName() + " expiration", e);
                    // 报异常,从缓存中移除key, 下一次就不会续期了.
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }

                // res为true的话,表示自动续期成功,继续递归调用,实现不断续期
                if (res) {
                    // 递归调用,实现不断续期

                    // reschedule itself
                    renewExpiration();
                } else {
                    // 续期失败,则取消定时任务, 移除key等
                    cancelExpirationRenewal(null);
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

从源码中可以看到,Redisson的看门狗借助了netty的时间轮,简单理解就是定时任务,对分布式锁进行自动续期的。

有几个关键点:

  • 1、延迟调度,延迟时间为:internalLockLeaseTime / 3,就是 10s 左右后会调度这个 TimerTask;

本例中,我们没有指定超时时间,internalLockLeaseTime默认就是在RedissonLock构造方法中获取的看门狗超时时间30秒,那么30 / 3 = 10秒,也就是看门狗将会延迟10秒启动第一次续期。

  • 2、异步续租:逻辑都在renewExpirationAsync(threadId)里面,后面详细分析;
  • 3、递归调用:当续租成功之后,重新调用renewExpiration()自己,从而达到持续续租的目的;

接下来我们看一下看门狗是如何实现自动续期的,核心代码在renewExpirationAsync(threadId)方法:

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
    /**
     * 底层还是通过LUA脚本实现: 通过hexists指令判断锁是不是自己的锁,如果是的话,则通过pexpire指令将锁的过期时间给重置了,返回1,表示自动续期成功;返回0,表示续期失败。
     */
    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));
}

可以看到,自动续期底层还是一段LUA脚本,通过hexists指令判断锁是不是自己的锁,如果是的话,则通过pexpire指令将锁的过期时间给重置为30秒,返回1,表示自动续期成功;返回0,表示续期失败。

过了 10s 左右,判断到线程还持有着这把锁,即业务还没执行完,就会将锁的时间重新设置为 30s,返回true,然后通过递归,又过了10s,再一次续期,不断循环这个过程,直到锁被释放或者其它一些情况判断到当前线程已经没有持有这把锁之后,取消看门狗定时任务。

简要总结一下看门狗机制:

  • 只有在未指定锁超时时间时才会使用看门狗;
  • 看门狗默认续租时间是 10s 左右,internalLockLeaseTime / 3;
  • 可以通过 Config 统一设置看门狗的时间,设置 lockWatchdogTimeout 参数即可;

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

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

相关文章

OpenGL基础图形编程(八)变换

八、OpenGL变换 OpenGL变换是本篇的重点内容&#xff0c;它包括计算机图形学中最基本的三维变换&#xff0c;即几何变换、投影变换、裁剪变换、视口变换&#xff0c;以及针对OpenGL的特殊变换概念理解和用法&#xff0c;如相机模拟、矩阵堆栈等。学好了这章&#xff0c;才开始真…

基于多时间尺度滚动优化的多能源微网双层调度研究附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

网络套接字编程(TCP协议)

文章目录简单的TCP网络程序服务器绑定服务端监听服务端获取连接客户端连接服务器多线程版本的大小写字母转换服务简单的TCP网络程序 int socket(int domain, int type, int protocol);参数说明&#xff1a; domain&#xff1a;创建套接字的域或者叫做协议家族&#xff0c;也就…

百万基建狂魔们的赛博世界

钉钉完成的&#xff0c;是基于PaaS底座和底层基础产品&#xff0c;与生态伙伴一起提供低代码的普惠化定制开发模式&#xff0c;让大型企业自己可以具备诊断自己的能力和梳理流程的能力&#xff0c;并且将过往的经验和积累进行数字化应用层面的表达&#xff0c;进而寻找出一条最…

ffplay调试环境搭建

前言 ffplay是基于FFmpeg的最简单的官方播放器。麻雀虽小&#xff0c;五脏俱全&#xff0c;虽说ffplay简单&#xff0c;但是各种播放器应有的功能一一俱全&#xff0c;说它简单或许仅仅是因为它只有一个点c文件而已吧。 想要开发一个优秀的播放器&#xff0c;参考是必不可少的&…

Netron可视化Pytorch保存的网络模型

目录 一.理清网络的输入与输出 二. 将模型转换为onnx格式 三.Netron可视化工具 一.理清网络的输入与输出 我自定义的网络模型&#xff08;主要看看前向传播函数即可&#xff09;&#xff1a; import torch import torch.nn as nn#导入数据预处理之后的相关数据 from dataP…

Acrel-EMS企业微电网能效管理平台在某食品加工厂35kV变电站应用-Susie 周

1、概述 该食品加工厂变电站工程规模&#xff1a;电压等级&#xff1a;35/10.5kV&#xff0c;规划主变容量16.3MVA1台8MVA。有一个总配电室&#xff0c;包括35kV开关柜、10kV开关柜和0.4kV配电柜&#xff0c;两个独立变压器室&#xff0c;变压器为干式变压器。35kV供电系统采用…

(2)ITK中迭代器的时间效率

背景 ITK对图像处理中&#xff0c;为了提高代码运行效率&#xff0c;通过迭代器Iterator可以实现对时间的优化。 在ITK的官方文档中也有明确的说明&#xff1a; 针对此说明&#xff0c;本次使用对图像获取最大值最小值的方式&#xff0c;来实验和测试其效率。 代码实现 &am…

JDBC 数据库连接池之Driud

1 数据库连接池简介 数据库连接池是个容器&#xff0c;负责分配、管理数据库连接(Connection) 它允许应用程序重复使用一个现有的数据库连接&#xff0c;而不是再重新建立一个&#xff1b; 释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据…

数据安全新战场,EasyMR为企业筑起“安全防线”

2020年1月&#xff0c;时间跨度长达14年的&#xff0c;微软2.5亿条客户服务和支持记录在网上泄露&#xff1b; 同年4月&#xff0c;微盟发生史上最贵“删库跑路”事件&#xff0c;造成微盟市值一夜之间缩水约24亿港币&#xff1b; 今年7月&#xff0c;网信办依据《数据安全法…

PCIEBPMCx4板卡

PCIEBPMCx4本板卡可以使标准的PMC板卡安装于带有PCIE插槽的PC机上使用&#xff0c;安装后占一个槽位&#xff0c;槽位可以为PCIE x4 PCIE x8、PCIE x16&#xff0c;安装后工作在PCIE x4模式。PCIE X1 后开口也可以使用&#xff0c;但只运行在PCIE X1模式。PCIE支持X4 V2.0,板载…

Python对json的操作总结

Json简介&#xff1a;Json&#xff0c;全名 JavaScript Object Notation&#xff0c;是一种轻量级的数据交换格式。Json最广泛的应用是作为AJAX中web服务器和客户端的通讯的数据格式。现在也常用于http请求中&#xff0c;所以对json的各种学习&#xff0c;是自然而然的事情。 J…

C++学习笔记(十四)——vector的模拟实现

vector各函数接口总览 vector当中的成员变量介绍 默认成员函数 构造函数1 构造函数2 构造函数3 拷贝构造函数 赋值运算符重载函数 析构函数 迭代器相关函数 begin和end 容量和大小相关函数 size和capacity reserve resize empty 修改容器内容相关函数 push_ba…

centos8:安装java

一、背景 因为centos 8 安装Jenkins需要java环境&#xff0c;所以本文记录安装java环境过程。 二、环境 开发电脑&#xff1a;Windows 10 CentOS 8.4 64位 三、安装 3.1、java -version检查是否已安装 java -version 没有安装 3.2、检查系统是否自带jdk rpm -qa |grep …

Word控件Spire.Doc 【超链接】教程(1):如何在C#/VB.NET中给Word 文档插入超链接

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

系统移植 uboot 2

一、uboot源码获取 1.1 uboot官网获取 ftp://ftp.denx.de/pub/u-boot/ 前提是是芯片厂家将uboot源码开源到uboot官网上 1.2 ST开发社区获取 https://wiki.stmicroelectronics.cn/stm32mpu/wiki/STM32MP1_Developer_Package 1.3 ST官网 https://www.st.com/en/embedded-sof…

opcj3—人人开源三大套件的简单用法

renren开源是一个很不错的开源开发组件&#xff0c;人人开源 其中目前对我们最有用的有三个&#xff1a;renren-fast、renren-fast-vue和renren-generator。 renren-generator是核心服务&#xff0c;可以根据数据库自动生成从controller层到service层&#xff0c;再到持久层的…

.net开发安卓入门 - 环境安装

文章目录工具VS2022Android SDK Manager如下图&#xff0c;安装一个镜像和工具模拟器设备管理器如下图启动模拟器&#xff0c;看一下效果常见问题工具 VS2022 下载地址&#xff1a;https://visualstudio.microsoft.com/zh-hans/thank-you-downloading-visual-studio/?skuCom…

Linux邮件服务Postfix部署

我们看下邮件协议&#xff1a; 简单邮件传输协议&#xff08;SMTP&#xff09;&#xff1a;用于发送和中转出的电子邮件。使用TCP/25端口。 邮局协议版本&#xff08;POP3&#xff09;&#xff1a;用于将邮件存储到本地&#xff0c;占用服务器的TCP/110端口。 Internet 消息访问…

【Python游戏】一个csdn小编用Python语言写了一个足球游戏,成功模拟世界杯决赛现场

前言 halo&#xff0c;包子们下午好 最近世界杯不是很火呀 很多小伙伴应该都知道球赛反正买&#xff0c;别墅靠大海&#xff01; 今天就给大家实现一个类似世界杯的足球小游戏&#xff0c;咱就说真的堪比国足了&#xff01; 哈哈哈~ 好啦 直接开整&#xff01;&#xff01;&am…