周四埋下的坑,周五来恶补!! Redisson 加锁、锁自动续期、解锁源码分析

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 周四埋下的坑,周五来恶补!! Redisson 加锁、锁自动续期、解锁源码分析

上一篇文章:用万字长文来讲讲本地锁至分布式锁的演进和Redis实现

在上一篇中挖了这个坑,今天就来把它填上哈~ ? 昨天聊到了Redisson实现了锁的自动续期,但是就简单提了一嘴就完事了。

今天就来多说一点点里面的实现和一些使用,它和我们自己的实现相比又如何。

文章大纲:

第一部分说了 Redisson简单使用

第二部分才是说Redisson底层源码如何实现分布式锁

1、 如何加锁

2、 如何实现锁自动续期,靠什么实现的?

3、 如何实现解锁

一、Redisson 简单使用

SpringBoot 中,因为自动装配的存在,使用某个封装好的轮子,就那么几步~

  • 导包
  • 编写配置
  • 编写xxxConfig
  • 准备开始使用它

1.1、导包

Redisson 也不例外

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>
 ​
 <dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson-spring-boot-starter</artifactId>
     <version>3.17.6</version>
 </dependency>

1.2、配置

这里的配置就是Redis的配置

 spring:
   redis:
     host: IP地址
     password: xxxx

1.3、编写配置类

那些什么最大连接数,连接超时等等就没配了,偷个懒~

 @Configuration
 public class MyRedissonConfig {
 ​
     /**
      * 所有对Redisson的使用都是通过RedissonClient
      * @return
      * @throws IOException
      */
     @Bean(destroyMethod="shutdown")
     public RedissonClient redissonClient() throws IOException {
         //1、创建配置
         Config config = new Config();
         config.useSingleServer().setAddress("redis://4IP地址:6379").setPassword("xxxx");
 //       config.setLockWatchdogTimeout();
         //2、根据Config创建出RedissonClient实例
         //Redis url should start with redis:// or rediss://
         RedissonClient redissonClient = Redisson.create(config);
         return redissonClient;
     }
 }

注意:这里面的setAddress()中的地址必须以redis://开头~

另外我这里只是设置了这么几个属性,很多我没去写了而已,不是它不能设置哈。

image.png

就比如修改锁自动需求的默认时间,就是

config.setLockWatchdogTimeout(); 锁自动续期默认时间是30s,这是可以被修改的。

其他的还需靠大家自己探索啦~

1.4、简单上手

最直接的使用:

 @RunWith(SpringRunner.class)
 @SpringBootTest(classes = {主启动类.class})
 public class RedisTest {
 ​
     @Autowired
     private RedissonClient redissonClient;
 ​
     @Test
     public void testRedisson() {
         // 获取锁实例
         RLock lock = redissonClient.getLock("redisson:lock");
         try {
             lock.lock();
             //执行需要加锁的业务~ 
         } finally {
             lock.unlock();
         }
     }
 }

这个lock()方法,它是可以填写参数,也可以不填的,

 public void lock()   
 public void lock(long leaseTime, TimeUnit unit) 

不填写参数,Redisson它帮助我们实现了锁的自动续期,

如果我们自己填写了锁的时间,Redisson则不会帮我们实现锁的自动续期。

这一点在后面分析源码的有详细的说明。

另外Redisson它作为分布式锁的实现,更好的方便Java开发者,它实现了JavaJUC包下的诸多接口,可以说使用起来完全没啥学习成本~

比如JUC下的读写锁,

 @Test
 public void testRedissonReadAndWriteLock() {
     RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("redisson:lock");= redissonClient.getReadWriteLock("redisson:lock");
     // 获取写锁
     RLock writeLock = readWriteLock.writeLock();
     // 获取读锁
     RLock readLock = readWriteLock.readLock();
     try {
         //上锁
         writeLock.lock();
         // 这种方式,使用我们自己定义的
         writeLock.lock(10,TimeUnit.SECONDS);
         //执行需要加锁的业务~
     } finally {
         writeLock.unlock();
     }
 }

更有其他的不少:

image.png详细的案例,其实在它的文档中都有说明的,有中文版本的,很简单的~

官方文档

这些该如何使用,其实就和用JUC下面的工具一样,懂了那一块的知识,就知道这里该如何使用啦,不懂的话,我说了也还是一样的哈哈~

让我偷个懒,可以去学一学JUC,蛮好玩的~

二、锁自动续期源码-看门狗机制分析

在分析之前,我们先把上一篇中分布式锁演进过程中所产生的问题都一一抛出来,之后再针对性的看看 Redisson 它又是如何解决的。

2.1、分布式锁演进的问题

为了方便其余没有看过看一篇文章的朋友,我把分布式锁中存在的几个问题,递推式的抽离出来了。

我们都知道要正确使用分布式锁,一定要注意原子性操作、死锁问题和被他人释放锁的问题和锁是否需要自动续期问题

  1. 死锁问题:说的是锁没有被动过期时间,即拿到了锁,但是在执行业务过程中,程序崩溃了,锁没有被正常释放,导致其他线程无法获取到锁,从而产生死锁问题。
    之前的解决方式:set nx ex命令
  2. 锁被其他人释放问题:第一条线程抢到锁,业务执行超时,第一条线程所持有的锁被自动释放;此时第二条线程拿到锁,准备执行业务,刚好第一条线程业务执行完成,照常执行了释放锁的过程,导致第二条线程持有的锁被第一条线程所释放,锁被其他人释放
    之前的解决方式:给锁一个唯一值(UUID),每次解锁前进行判断
  3. 原子性操作:原子性的意思就是要么都成功,要么都失败。像我们获取锁,设定值和设定时间是两步操作,让他们变成原子性操作就是设定值和设定时间成为一体,一起成功或者一起失败。另外解锁操作的获取锁,判断锁是否为当前线程所拥有,也必须是一个原子性操作。
    之前的解决方式:利用Redis中的lua脚本实现。
  4. 锁自动续期问题:这个就是利用了我们今天的Redisson来实现的。

2.2、分析的开端

我们就以最简单的流程

 @Test
 public void testRedisson() {
     RLock lock = redissonClient.getLock("redisson:lock");
     try {
         lock.lock();
         //执行需要加锁的业务~
     } finally {
         lock.unlock();
     }
 }

来分析以下几个问题:

  1. Redisson 是如何实现加锁的
  2. Redisson 是如何实现锁自动续期的
  3. Redisson 是如何解锁的

以及在这个过程中,去看看 Redisson它是如何解决我们之前出现的问题的。

顺着看下去~

 RLock lock = redissonClient.getLock("redisson:lock");
 lock.lock();

看似加锁操作只调用了一个lock()方法,但实际上流程走的可多了~

不多说,直接在Idea中点击跳转到RedissonLock中的类中

image.png

补充说明:这下面还有一些实现自旋的操作,就是写了一个while(true)来实现等待获取锁的代码,没有继续往下分析了。中间还有一些判断锁是否可以被打断、订阅和退订等操作,没有去仔细研究啦,大家感兴趣可以再往下多看看。


接着往下走👇

image.png

2.3、Redisson 如何实现加锁-tryLockInnerAsync()

漏分析了一步,不是从scheduleExpirationRenewal(threadId)前进到下一步,这说的是锁续期问题~

忘记说加锁啦~ 加锁就是tryLockInnerAsync()方法

点进去看看~

image.png

补充说明:

在这里可以看到几个方面:Redisson底层也是使用Lua脚本来实现的,这段 lua 脚本分为三个部分:

  1. 第一部分:加锁
    首先用 exists 判断了 KEYS[1] (即 redisson:lock)是否存在。 如果不存在,则进入第 5 行,使用 hincrby 命令创建一个新的哈希表,如果域field不存在,那么在执行命令前会被初始化为0,此命令的返回值就是执行hincrby命令后,哈希表key中域field的值,此时进行increment,也就是返回1,之后进入第6行,对KEY[1]设置过期时间,30000ms然后返回nil。
  2. 第二部分:重入
    首先判断KEY[1]是否存在,因为KEY[1]是一个hash结构,所以13行意思是获取这个KEYS[1]中字段为ARGV[2]也就是UUID:thredId这个值是否存在。
    如果存在进入14行代码对其进行加1操作(锁重入) 然后进入15行重新设置过期时间30s 然后返回nil
  3. 第三部分:返回:作用就是返回 KEY[1] 的剩余存活时间

此处的getRawName()方法就是我们获取到我们设定的锁名。如此处就是获取到Redisson:lock

getLockName(threadId)就是获取一个UUID:threadId的字符

串。

image.png

至于说此处的UUID,是如何来的,我稍微浅看了一下,这是初始化的时候给每个连接管理器都传了一个UUID的类,更具体的使用,没去追啦。

咱们知道这是 Redisson 也是采取一样的方式,它的值也是存了一个UUID,这点同样也可以在连接工具上查看到的。

image.png

看到这里就已经可以看出Redisson已经解决我们昨天出现的全部问题了。

首先是加锁操作的原子性是满足的了,因为 Redisson使用的Lua脚本将设置值和设置时间的操作变为一步;其次是之前的锁被其他人释放的问题,在这里Redisson也采用了唯一keyUUID来解决此问题。

那么剩下的就只有锁续期问题了,我们接着往下看👇

2.4、Redisson 如何实现锁续期

从这个scheduleExpirationRenewal(threadId);方法开始继续往下探索~

image.png

image.png

我们回过头来接着看 续期方法renewExpiration()

看它底层是如何实现的。

image.png

可以从这段代码中很明显的看出,是启动了一个定时任务,该任务每 internalLockLeaseTime/3ms 后执行一次。而 internalLockLeaseTime默认为 30000。所以该任务每 10s 执行一次。


renewExpirationAsync(long threadId)方法,就是实现锁重新续期的lua脚本的执行

image.png

上面的定时任务在不修改看门狗的默认时间时,就是每10s执行一次,意思就是每次在锁还剩下20s时,就会执行这段重新续期的代码,让锁重新续期到30s。

不知道说到这里,大伙有听懂吗~

2.5、Redisson 如何实现解锁操作

其实看懂了Redisson是如何加锁的,其实看这解锁操作也是特别容易的。

不过我们这次要回到开始分析的地方去啦。

image.png

看的出来,解锁操作并不复杂,我们先去看看unlockInnerAsync(threadId);方法吧。

image.png

其实看完加锁,再看这个其实都能理解啦吧

  • 首先判断KEYS[1]是否存在
  • 存在将值减1,如果counter还大于0,就重新设置过期时间30000ms,否则就删除操作
  • redis.call('publish', KEYS[2], ARGV[1]);同时发布了一个事件,这个是干嘛的?是去通知正在等待获取锁其他的线程们,可以使用这把锁了。

image.png

另外cancelExpirationRenewal(threadId);方法就是一些取消和删除操作。

image.png

总结

Redisson 相比较我们自己的实现如何?

首先可以说的是,这个轮子考虑的比我们周到的多,在上篇说的各种问题都是解决了的。

在各种加锁或者解锁操作上都实现了原子性~

几个点:

  1. 在使用 Redisson 获取锁的过程,你主动设定了锁的过期时间,`Redisson 将不会开启看门狗机制。
  2. Redisson 在 Redis 中保存的结构是一个 Hash的数据结构,key 的名称是我们的锁名称,如案例中使用的 redisson:lock,存储的字段名称为 UUID:threadId,值的话就是 1
  3. 结构如下图:
  4. Redisson实现锁自动续期的底层是开启了一个线程,异步的执行定时任务,在锁还剩下20s,自动续期为30s,此定时任务是采用Netty框架中的时间轮算法实现。

image.png

今天就看到这里啦,周四挖的坑,周五熬夜终于写出来了。

后记

不知道这篇文章有没有帮助到你,希望看完的你,对于Redisson已经没有那么恐惧~,当然它其实也不难。

如果觉得有收获的话,记得点点赞,给我来个关注吧~ 😁

image.png

目录
相关文章
|
NoSQL Java 网络安全
Redisson官方文档 - 2. 配置方法
Redisson客户端配置方法
33480 0
|
12月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
10月前
|
供应链 NoSQL Java
关于Redisson分布式锁的用法
Redisson分布式锁是实现分布式系统中资源同步的有效工具。通过合理配置和使用Redisson的各种锁机制,可以确保系统的高可用性和数据一致性。本文详细介绍了Redisson分布式锁的配置、基本用法和高级用法,并提供了实际应用示例,希望对您在实际项目中使用Redisson分布式锁有所帮助。c
1479 10
|
12月前
|
NoSQL Java API
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
解决lambda中必须为final的方式
解决lambda中必须为final的方式
673 0
|
SQL druid Java
解决 ‘The last packet successfully received from the server was xxx milliseconds ago‘ 问题
解决 ‘The last packet successfully received from the server was xxx milliseconds ago‘ 问题
6456 0
|
安全 微服务
十二.SpringCloud+Security+Oauth2实现微服务授权 - 资源服务器配置
SpringCloud+Security+Oauth2实现微服务授权 - 资源服务器配置
|
缓存 监控 Java
深入解析Nacos配置中心的动态配置更新技术
深入解析Nacos配置中心的动态配置更新技术