Redisson的看门狗机制

news2024/11/18 9:26:19

背景

据Redisson官网的介绍,Redisson是一个Java Redis客户端,与Spring 提供给我们的 RedisTemplate 工具没有本质的区别,可以把它看做是一个功能更强大的客户端(虽然官网上声称Redisson不只是一个Java Redis客户端)

强烈推荐下阅读redisson的中文官网

我想我们用到 Redisson 最多的场景一定是分布式锁,一个基础的分布式锁具有三个特性:

互斥:在分布式高并发的条件下,需要保证,同一时刻只能有一个线程获得锁,这是最最基本的一点。
防止死锁:在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。
可重入:我们知道ReentrantLock是可重入锁,那它的特点就是同一个线程可以重复拿到同一个资源的锁。
实现的方案有很多,这里,就以我们平时在网上常看到的redis分布式锁方案为例,来对比看看 Redisson 提供的分布式锁有什么高级的地方。

普通的 Redis 分布式锁的缺陷

我们在网上看到的redis分布式锁的工具方法,大都满足互斥、防止死锁的特性,有些工具方法会满足可重入特性。

如果只满足上述3种特性会有哪些隐患呢?redis分布式锁无法自动续期,比如,一个锁设置了1分钟超时释放,如果拿到这个锁的线程在一分钟内没有执行完毕,那么这个锁就会被其他线程拿到,可能会导致严重的线上问题,我已经在秒杀系统故障排查文章中,看到好多因为这个缺陷导致的超卖了。

Redisson 提供的分布式锁

watch dog 的自动延期机制

Redisson 锁的加锁机制如上图所示,线程去获取锁,获取成功则执行lua脚本,保存数据到redis数据库。

如果获取失败: 一直通过while循环尝试获取锁(可自定义等待时间,超时后返回失败),获取成功后,执行lua脚本,保存数据到redis数据库。

Redisson提供的分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 机制。

同时 redisson 还有公平锁、读写锁的实现。

使用样例如下,附有方法的详细机制释义

private void redissonDoc() throws InterruptedException {
    //1. 普通的可重入锁
    RLock lock = redissonClient.getLock("generalLock");

    // 拿锁失败时会不停的重试
    // 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
    lock.lock();

    // 尝试拿锁10s后停止重试,返回false
    // 具有Watch Dog 自动延期机制 默认续30s
    boolean res1 = lock.tryLock(10, TimeUnit.SECONDS);

    // 拿锁失败时会不停的重试
    // 没有Watch Dog ,10s后自动释放
    lock.lock(10, TimeUnit.SECONDS);

    // 尝试拿锁100s后停止重试,返回false
    // 没有Watch Dog ,10s后自动释放
    boolean res2 = lock.tryLock(100, 10, TimeUnit.SECONDS);

    //2. 公平锁 保证 Redisson 客户端线程将以其请求的顺序获得锁
    RLock fairLock = redissonClient.getFairLock("fairLock");

    //3. 读写锁 没错与JDK中ReentrantLock的读写锁效果一样
    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock");
    readWriteLock.readLock().lock();
    readWriteLock.writeLock().lock();
}

如何启动Redisson的看门狗机制

如果你想让Redisson启动看门狗机制,你就不能自己在获取锁的时候,定义超时释放锁的时间,无论,你是通过lock() (void lock(long leaseTime, TimeUnit unit);)还是通过tryLock获取锁,只要在参数中,不传入releastime,就会开启看门狗机制,
就是这两个方法不要用: boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
和void lock(long leaseTime, TimeUnit unit);,因为它俩都传release

但是,你传的leaseTime是-1,也是会开启看门狗机制的,具体在源码部分解释

watch dog 核心源码解读

如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,加入一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,Redisson给出了自己的答案,就是 watch dog 自动延期机制。

其实,这个例子就很容易让人误导,这个30秒不是你传的leaseTime参数为30,而是你不传leaseTime或者传-1时,Redisson配置中默认给你的30秒

我在学习redis分布式锁的时候,一直有一个疑问,就是为什么非要设置锁的超时时间,不设置不行吗?于是,我就反向思考,不设置锁超时的话,会出现什么问题?

当一个线程A在获取redis分布式锁的时候,没有设置超时时间,如果在释放锁的时候,出现了异常,那么锁就会常驻redis服务中,当另外一个线程B获取锁的时候,无论你是通过自定义的redis分布式锁setnx,还是通过Redisson实现的分布式锁的方式**if (redis.call(‘exists’, KEYS[1]) == 0) **,在获取锁之前,其实都有一个逻辑判断:如果该锁已经存在,就是key已经存在,就不往redis中写了,也就是获取锁失败
那么线程B就永远不会获取到锁,自然就一直阻塞在获取锁的代码处,发生死锁
如果有了超时时间,异常发生了,超时的话,redis服务器自己就把key删除了,也就是锁释放了
这也就避免了并发下的死锁问题

有了这么一层逻辑,你就会明白,为什么我们不传release超时释放锁时间,Redisson也会给我们默认传一个30秒的锁超时释放时间了

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

默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。

另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。

watch dog 核心源码解读

 // 直接使用lock无参数方法
public void lock() {
    try {
        lock(-1, null, false);
    } catch (InterruptedException e) {
        throw new IllegalStateException();
    }
}

// 进入该方法 其中leaseTime = -1
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
    // lock acquired
    if (ttl == null) {
        return;
    }

   //...
}

// 进入 tryAcquire(-1, leaseTime, unit, threadId)
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}

// 进入 tryAcquireAsync
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 = -1 时 启动 watch dog机制
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                            commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                            TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    //执行完lua脚本后的回调
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }

        if (ttlRemaining == null) {
            // watch dog 
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

从源码中可以得知,如果不传release,默认会给个-1,如果release是-1的话,通过 if (leaseTime != -1) 判断就会开启看门狗机制,这也是为啥我说,无论你是tryLock还是Lock只要不传release,就会开启看门狗机制,所以,如果你想解决由于线程执行慢或者阻塞,造成锁超时释放的问题,就不要在两个方法中传release,实际上,通过传release参数来设置超时时间,风险是比较大的,你需要清楚的知道,线程执行业务的时间,设置的过小,redis服务器就自动给你释放了

scheduleExpirationRenewal 方法开启监控:

private 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);
        
        // 第一次获得锁 延期操作
        renewExpiration();
    }
}

// 进入 renewExpiration()
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;
            }
            
            //执行lua 进行续期
            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=10秒检查一次
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

//lua脚本 执行包装好的lua脚本进行key续期
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));
}

关键结论
上述源码读过来我们可以记住几个关键情报:

watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
从可2得出,如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;
看到3的时候,可能会有人有疑问,如果释放锁操作本身异常了,watch dog 还会不停的续期吗?下面看一下释放锁的源码,找找答案

// 锁释放
public void unlock() {
    try {
        get(unlockAsync(Thread.currentThread().getId()));
    } catch (RedisException e) {
        if (e.getCause() instanceof IllegalMonitorStateException) {
            throw (IllegalMonitorStateException) e.getCause();
        } else {
            throw e;
        }
    }
}

// 进入 unlockAsync(Thread.currentThread().getId()) 方法 入参是当前线程的id
public RFuture<Void> unlockAsync(long threadId) {
    RPromise<Void> result = new RedissonPromise<Void>();
    //执行lua脚本 删除key
    RFuture<Boolean> future = unlockInnerAsync(threadId);
    //回调函数
    future.onComplete((opStatus, e) -> {
        // 无论执行lua脚本是否成功 执行cancelExpirationRenewal(threadId) 方法来删除EXPIRATION_RENEWAL_MAP中的缓存
        cancelExpirationRenewal(threadId);

        if (e != null) {
            result.tryFailure(e);
            return;
        }

        if (opStatus == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            result.tryFailure(cause);
            return;
        }

        result.trySuccess(null);
    });

    return result;
}

// 此方法会停止 watch dog 机制
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());
    }
}

释放锁的操作中 有一步操作是从 EXPIRATION_RENEWAL_MAP 中获取 ExpirationEntry 对象,然后将其remove,结合watch dog中的续期前的判断:

EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
    return;
}

可以得出结论:

如果释放锁操作本身异常了,watch dog 还会不停的续期吗?不会,因为无论释放锁操作是否成功,EXPIRATION_RENEWAL_MAP中的目标 ExpirationEntry 对象已经被移除了,watch dog 通过判断后就不会继续给锁续期了。

因为无论在释放锁的时候,是否出现异常,都会执行释放锁的回调函数,把看门狗停了

有没有设想过一种场景?服务器宕机了?其实这也没关系,首先获取锁和释放锁的逻辑都是在一台服务器上,那看门狗的续约也就没有了,redis中只有一个看门狗上次重置了30秒的key,时间到了key也就自然删除了,那么其他服务器,只需要等待redis自动删除这个key就好了,也就不存在死锁了

关键结论

watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
可以修该watchDog设置的30秒的时间,这也是我推荐的不传releas,设置锁超时的方式

watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;

如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;

要使 watchLog机制生效 。只要不穿leaseTime即可

watchlog的延时时间 可以由 lockWatchdogTimeout指定默认延时时间,但是不要设置太小。如100
watchdog 会每 lockWatchdogTimeout/3时间,去延时。
watchdog 通过 类似netty的 Future功能来实现异步延时
watchdog 最终还是通过 lua脚本来进行延时

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

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

相关文章

记录Maven的相关操作(笔记整理)

一、安装 我使用的是免安装版的&#xff0c;直接解压缩就可以使用。 二、配置环境变量 打开环境变量配置。右键计算机→属性→高级系统设置→高级→环境变量&#xff0c;在系统变量中配置。 配置MAVEN_HOME。在系统变量中新建&#xff0c;变量名MAVEN_HOME&#xff0c;变量值…

parquet

一、parquet结构 Row Group ​ --Column Chunk&#xff1a;一列对应一个Column Chunk ​ – Page&#xff1a;压缩和编码的单元&#xff0c;parquet的 min/max 索引是针对于page的&#xff0c;存在了文件的页脚。以前的版本是存储Column Chunk和Page的索引&#xff0c;导致在…

Linux命令scp用法

本文主要讲的是scp用法如果哪里不对欢迎指出&#xff0c;主页https://blog.csdn.net/qq_57785602?typeblog首先讲述一下scp用法并不是让你连接公司服务器后用的&#xff08;不是连接公司服务器使用&#xff09;&#xff0c;如果要使用的情况下那么请看下面&#xff1a;winr打开…

【C语言篇】请把这篇文章推给现在还对指针一知半解的童鞋~超生动图解,详细讲解,易懂,易学,让天下没有难懂的指针~

&#x1f331;博主简介&#xff1a;是瑶瑶子啦&#xff0c;一名大一计科生&#xff0c;目前在努力学习C进阶,JavaSE。热爱写博客~正在努力成为一个厉害的开发程序媛&#xff01; &#x1f4dc;所属专栏&#xff1a;C/C ✈往期博文回顾:进入内存,透彻理解数据类型存在的意义,整形…

[java拓展]Mysql数据库的基础指令,和JDBC的使用

1.关于mysql数据库 &#xff08;1&#xff09;概述 DBMS数据库管理系统&#xff0c;用来管理数据库&#xff0c;执行sql语句的东西&#xff0c;Mysql&#xff0c;oracle&#xff0c;sqlite这些严格来说不是数据库而是数据库管理系统&#xff0c;其中Mysql最常用&#xff0c;而…

马来酰亚胺聚乙二醇硅烷,MAL-PEG-Silane 结构,科研试剂溶于大部分有机溶剂

马来酰亚胺聚乙二醇硅烷&#xff0c;MAL-PEG-Silane 中文名称&#xff1a;马来酰亚胺聚乙二醇硅烷 分子量&#xff1a;1k&#xff0c;2k&#xff0c;3.4k&#xff0c;5k&#xff0c;10k&#xff0c;20k。。。 存储条件&#xff1a;-20C&#xff0c;避光&#xff0c;避湿 用…

剑指offer----C语言版----第十九天----面试题25:合并两个有序的链表

目录 1. 合并两个排序的链表 1.1 题目描述 1.2 解题思路 1.3 往期回顾 1. 合并两个排序的链表 原题链接&#xff1a;21. 合并两个有序链表 - 力扣&#xff08;LeetCode&#xff09;1.1 题目描述 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个…

new-Crack:StarUML 5.1.0 -2023

StarUML 用于敏捷和简洁建模的复杂软件建模器 当前版本&#xff1a;v5.1.0 主要特征 通用语言2 兼容 UML 2.x 标准元模型和图表&#xff1a;类、对象、用例、组件、部署、复合结构、序列、通信、状态图、活动、时间、交互溢出、信息流和剖面图。 SYSML 支持 支持使用 SysML 图…

SSM03_SpringMVC REST风格 SSM整合

01-SpringMVC简介SpringMVC是隶属于Spring框架的一部分&#xff0c;主要是用来进行Web开发&#xff0c;是对Servlet进行了封装。SpringMVC是处于Web层的框架&#xff0c;所以其主要的作用就是用来接收前端发过来的请求和数据然后经过处理并将处理的结果响应给前端&#xff0c;所…

【iHooya】1月14日寒假集训课作业解析

内部元素之和 输入一个整数矩阵&#xff0c;计算位于矩阵内部的元素之和。所谓矩阵内部的元素&#xff0c;不在第一行和最后一行的元素以及第一列和最后一列的元素。 输入 第一行分别为矩阵的行数m和列数n&#xff08;m < 100&#xff0c;n < 100&#xff09;&#xff0…

Arthas 入门到实战(一)快速入门

Arthas官方文档指出&#xff1a; 介绍&#xff1a;Arthas 是一款线上监控诊断产品&#xff0c;通过全局视角实时查看应用 load、内存、gc、线程的状态信息&#xff0c;并能在不修改应用代码的情况下&#xff0c;对业务问题进行诊断&#xff0c;包括查看方法调用的出入参、异常&…

人工智能与游戏

游戏是智能应用最好的平台&#xff0c;可惜的是&#xff1a;只用了少部分计算AI&#xff0c;还没有用到智能的计算计1 引言从1950年香农教授提出为计算机象棋博弈编写程序开始&#xff0c;游戏人工智能就是人工智能技术研究的前沿&#xff0c;被誉为人工智能界的“果蝇”&#…

微信小程序070校园食堂订餐多商家带配送

基于微信小程序食堂订餐管理系 系统分为用户和管理员&#xff0c;商家三个角色 用户小程序端的主要功能有&#xff1a; 1.用户注册和登陆小程序 2.查看系统新闻资讯公告 3.用户查看小程序端的菜品信息&#xff0c;在线搜索菜品&#xff0c; 4.用户查看菜品详情&#xff0c;收…

数据结构-二叉搜索树解析和实现

1.含义规则特性二叉搜索树也叫排序二叉树、有序二叉树&#xff0c;为什么这么叫呢&#xff1f;名字由来是什么&#xff1f;主要是它的规则图一规则一&#xff0c;左子树的所有节点的值均小于它的根节点的值规则二&#xff0c;右子树的所有节点的值均大于它的根节点的值&#xf…

摇头测距小车01_舵机和超声波代码封装

目录 一、摇头测距小车图片演示 二、接线方式 三、代码实现 一、摇头测距小车图片演示 就是在小车原有的基础上&#xff0c;在小车前面加一个舵机和一个超声波&#xff0c;把超声波粘在舵机上 二、接线方式 1、超声波接线 VCC-----上官一号5V GND----上官一号GND Trig----…

.net开发安卓入门-Dialog

.net开发安卓入门-DialogAndroid.App.AlertDialog运行效果代码UI源码引入 下面这个类库才可以使用Java.Interop.Export特性绑定事件Android.App.AlertDialog https://learn.microsoft.com/zh-cn/dotnet/api/android.app.alertdialog?viewxamarin-android-sdk-13 SetTitle &…

nodejs+vue摄影跟拍预定管理系统

&#xff0c;本系统分为用户&#xff0c;摄影师&#xff0c;管理员三个角色&#xff0c;用户可以注册登陆系统&#xff0c;查看摄影套餐&#xff0c;预约套餐&#xff0c;购买摄影周边商品&#xff0c;查看跟拍照片等。摄影师可以对用户的摄影预约审核&#xff0c;跟拍流程管理…

【Java集合】ArrayList自动扩容机制分析

目录 先从 ArrayList 的构造函数说起 一步一步分析 ArrayList 扩容机制 先来看 add 方法 再来看看 ensureCapacityInternal() 方法 ensureExplicitCapacity()和calculateCapacity方法 下面我们接着来看grow() 方法 再来看一下grow()中调用的hugeCapacity() 方法 System.arrayco…

3、代码注释与编码规范

目录 一、代码注释 &#xff08;1&#xff09;单行注释 &#xff08;2&#xff09;多行注释 &#xff08;3&#xff09;文档注释 2. 编码规范 一、代码注释 &#xff08;1&#xff09;单行注释 “//”为单行注释标记&#xff0c;从符号“//”开始直到换行为止的所有内容…

3D立体字生成器【免费在线工具】

Text2STL是一个可以在线使用的免费的3D立体字生成工具&#xff0c;输入文字内容即可实时预览生成的3D立体字模型&#xff0c;还可以导出为STL模型用于3D打印&#xff1a; 3D立体字生成器访问地址&#xff1a; http://text2stl.bimant.com/zh-cn/generator 1、3D立体字生成风…