Bootstrap

Redisson快速入门及可重入,可重试,看门狗机制原理

Redisson

一.redis 分布式锁的局限性

基于上次发文redis的分布式锁,已经可以应对大部分的业务场景,但是我们还可以进一步优化它.从它的缺点我们可以一步步进行分析.

  • 不可重入: 方法一执行时,获取到分布式锁,里面调用方法二,而方法二也需要获取分布式锁,由于方法一执行时已经获取到了分布式锁,所以此时方法二同样需要获得分布式锁时,会造成拿不到的现象,这样就造成了循环等待,方法二在等方法一将分布式锁释放,方法一在等方法二执行完毕再去释放锁.造成了这种死锁现象,也就是锁的不可重入导致的问题.
  • 不可重试: 获取不到锁以后,没有重试的机制,重试如果采用递归会存在资源浪费的问题,而且万一业务出现阻塞,锁一直得不到释放,如果采用递归的方法去获取锁,此时容易造成栈溢出的现象,不安全也不可取.所以我们应该如何合理的确认我们的重试机制.
  • 超时设置: 超时设置时间多少为合适,设置短了容易造成业务还没有完成锁就得到了释放,设置长了导致业务性能的影响下降,也导致数据的不一致性时间过长.如何合适的设置超时时间也成为一个难点.
  • 主从一致:如果Redis提供了主从集群,主从同步存在延迟,当主节点宕机时,此时还没有将锁同步到从节点,通过哨兵机制,主从转化,而此时从节点并没有这把锁,所以别的线程就可以获取到锁,产生问题.

二.Redission的快速入门

我们简单的快速入门一下,如何去使用Redisson的用法

首先引入依赖


    <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson</artifactId>
                <version>3.13.6</version>
    </dependency>

配置RedissonClient


    @Configuration
    public class RedissonConfig {
    
        @Bean
        public RedissonClient redissonClient() {
            //1.创建配置对象
            Config config = new Config();
            //2.设置连接地址和密码,填入你的redis的ip和端口号,并设置密码
            config.useSingleServer().setAddress("redis://ip:port").setPassword("password");
            //3.创建Redisson客户端对象
            return Redisson.create(config);
        }
    }
    

设置锁,尝试获取锁和释放锁

    {
    		RLock lock = redissonClient.getLock("order:" + userId);
            //无参, 尝试获取锁,不等待直接返回
        		
            //三个参数,重试时间,锁的超时时间,单位
            boolean isLock = lock.tryLock();
        	
        	//释放锁,将其放入finally
        	lock.unlock();
    }

三.可重入原理

先简单解释一下什么叫可重入锁:

  • 概念: 可重入锁是一种支持同一个线程多次获取同一个锁的锁机制。当一个线程获得了锁之后,如果再次尝试获取同一个锁时,可重入锁会允许该线程继续获取锁而不会被阻塞.

那我们如何自己去利用redis怎样实现这样一个可重入锁呢?

首先如果我们像原来的逻辑,采用String类型的结构设置锁,这样线程一拿到锁以后,绝对不可能再次拿到这把锁,因为key相同判定锁已存在,获取必然失败.所以我们得想另一种方式.

redis中还有另一种结构,hash结构,我们以锁的名称做键值, 而以前缀+线程id 作为hash结构的键值, 采用计数器的思想作为值,也就是如果同样一个线程获取锁,我们只增加获取这把锁的获取次数,然后释放锁时检查是不是自己线程的这把锁,如果是数量就-1,当减为0时,锁就可以被释放.这样不就可以满足可重入锁的需求了吗?值得注意的是,我们应该每次获取锁之后和释放锁之前,都需要重置锁的有效时间.这样才能方便下一个业务能够有足够的时间去完成.

流程图大概是这样.
在这里插入图片描述

我们来跟踪Redission的源码来看看Redission是怎么做的
先来解释一下它的各个方法参数的含义
在这里插入图片描述
分别对应无参,两个参数,三个参数的情况
在这里插入图片描述

跟进源码查看,先查看tryLock的上部分(waitTime为重试等待时间,leaseTime为超时时间,TimeUnit表示时间单位)

在这里插入图片描述

跟进tryAcquire
在这里插入图片描述

继续跟进
在这里插入图片描述

查看tryLockInnerAsync
在这里插入图片描述

我们发现了一段Lua脚本,那这段脚本执行的是什么意思呢?
在这里插入图片描述

我这里给大家打了注释方便查看.实际上也就是对上了我们之前的逻辑,我们为了这其中这里逻辑的原子性,所以我们采用了lua脚本.总的来说第一部分就是没有锁,则创建锁,跟哪个业务有关的锁作为key值,而前缀+线程id作为hashkey值,用来标识哪个服务器中的线程,成为唯一标识,值递增+1,表明线程获取锁一次.

第二部分就是,锁已经存在,看看这把锁是不是自己这个线程的锁,如果是,表示可以重入,则值++,表示这个线程再次获取锁一次.然后同样重置锁的超时时间,返回nil.

第三部分就是没有获取到锁,也就是其他线程来获取,我们返回了一个锁的释放时间,为什么要返回释放时间呢?这跟重试机制有关,我们暂且先忽略这点.

总结就是返回nil(在java中就是Null)表明获取到锁,其他的就是没有获取到.

我们再来看看释放锁的源码,实际上也是Lua脚本.

在这里插入图片描述

解释一下这段Lua脚本
在这里插入图片描述
同样对应着我们上方流程图的逻辑,释放锁时先检查是不是自己线程的这把锁,如果不是,直接返回Nil.如果是,将锁的重入次数-1,随后判断锁的重入次数,如果不为0,说明还有其他业务没有完成,锁还在被这个线程获取着,那么刷新锁的超时时间,如果为0了,说明这个线程所有获取锁的动作已经全部完成,要将锁释放,所以此时执行删除锁的逻辑.并推送锁被释放的消息,欸,这里为什么要推送锁被释放的消息呢?这跟可重试有关,我们待会再来介绍这一点.

至此,可重入锁的原理分析完了,依据源码我们能更好的去结合我们的流程去理解这部分的原理.

四.可重试原理

接下来到我们的锁重试机制,获取锁失败后,能否以更好的决策去再次获取锁?

我们来看看Redisson怎么去实现的,同样的跟进源码查看

我们进入两个参数的trylock方法
在这里插入图片描述
查看到
在这里插入图片描述
我们来分析后半部分代码

在这里插入图片描述

这里提到了一个订阅任务,说明Redission没有采用直接重试,而是等待,等待什么呢?等待释放锁的信号到来,才开始进行重试.我们回忆之前的释放锁的源码

在这里插入图片描述

现在我们就很清楚的知道释放锁之后这个消息为什么要发布出来.

回到原来的代码,我们接着分析,当锁的释放消息发送出来,返回为true,则会执行下列的代码.

在这里插入图片描述

然后紧接着
在这里插入图片描述

至此锁重试的原理也就分析完毕啦

五.看门狗机制

我们还有一个问题,就是锁的释放时间我们究竟改设置为多少呢?如果少了,业务可能还没有执行完毕锁就得到了释放,设置长了,影响性能,别的线程都获取不了锁,只能重试等待.

我们看看Redisson怎么做的.

当我们没有传递自定义的锁的超时时间时,默认leaseTime为-1.将执行第二段代码
在这里插入图片描述
这里有一个getLockWatchdogTimeout(),好奇怪的方法名,获得看门狗的超时时间,点进去看看
在这里插入图片描述

原来是一个默认的锁超时释放时间,默认为30s.

同样的在回顾之前获取锁的源码有一段这样的代码我们没有分析.
在这里插入图片描述

我们在尝试获取锁之后,返回了一个RFuture接口的对象,这里为ttlRemainningFuture,根据名字我们想一想,未来剩余超时时间,应该是个跟时间有关的变量,还记得我们获取锁的lua脚本吗
在这里插入图片描述

发现只要是获取到了锁,返回的都是nil,也就是为null,我们仔细看看这段代码

在这里插入图片描述
这是个异步调用的函数,当尝试获取锁完毕之后会执行,ttlRemaining表示尝试获取锁的结果,e表示异常信息.

而我们上文讲到只要获取到锁返回的都是null,也就是ttlRemaining== null ,所以我们会进入这段代码的下面这个区域,

进入源码查看

在这里插入图片描述

这里解释一下getEntryName是什么
在这里插入图片描述

接下来跟进我们刚刚分析的renewExpiration方法.
在这里插入图片描述

这里调用了一个renewExpirationAsync(threadId),我们再次跟进看看
在这里插入图片描述

原来是执行了重置锁的超时释放时间.回到原来的代码层
在这里插入图片描述

也就是总结而言,当尝试获取锁之后,会自动的开启定时任务,如果发现10s之后,业务还没有执行完,将会重置锁的超时释放时间.循环往复直到锁被释放.那这个定时任务什么时候被取消呢,我们也可以去查看锁释放的那里逻辑

在这里插入图片描述

锁被释放后,将会取消这里的定时任务,所以这样就避免我们因为业务需要执行时间过长而导致锁的释放.

注意: 看门狗机制只有在你不设置锁的释放时间时才会生效,也就是leastTime为-1时.

;