【Redisson分布式锁】Redisson读写锁加锁机制分析

欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!

在我后台回复 「资料」 可领取编程高频电子书
在我后台回复「面试」可领取硬核面试笔记

文章导读地址:点击查看文章导读!

感谢你的关注!

Redisson读写锁加锁机制分析

在这里插入图片描述

前几篇说了 Redisson 的可重入锁和公平锁是如何实现的

这里来讲一下 Redisson 的读写锁是如何实现的,这里在具体学习源码的时候,不要去具体扣他每一行的命令到底是执行的什么操作,扣这些细节是没有意义的

那么我们要学习源码中的哪些内容呢?

主要是要学习它的 设计思想 ,也就是为了实现功能做了哪些设计,以及实现的 流程 ,了解原理就好了!

image-20240308105523803

读锁加锁流程

这里我们先来看读写锁中的 读锁 的加锁流程,首先还是先从调用入口进入:

public static void main(String[] args) throws InterruptedException {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456").setDatabase(0);//获取客户端RedissonClient redissonClient = Redisson.create(config);// 获取读写锁RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("11_come");// 获取读锁readWriteLock.readLock().lock();//关闭客户端redissonClient.shutdown();
}

这里其实 lock() 方法还是进入了 RedissonLock 的 lock() 方法,而读锁和之前说的可重入锁的区别就在于最底层加锁的方法,也就是最终的 tryLockInnerAsync() 方法不同,接下来进入 读写锁的加锁方法 查看:

image-20240307203259568

整体的加锁方法如上,这里先说参数含义,之后来说一下这个 lua 脚本在做什么事情:

  • KEYS[1] :读写锁的名称, 也就是 11_come
  • KEYS[2] :读锁超时时间的前缀,用来表示每一把读锁,也就是 {11_come}:23d25595-6532-4105-a24b-98fea0997bc5:1:rwlock_timeout
  • ARGV[1] :锁的释放时间,默认是 30000ms ,也就是 30s
  • ARGV[2] :锁的标识,也就是当前客户端线程的唯一标识,UUID+threadId ,即 23d25595-6532-4105-a24b-98fea0997bc5:1

接下来在说 lua 脚本之前,先说一下 读写锁的特性 ,即:

  • 读锁和读锁之间不互斥
  • 读锁和写锁之间互斥
  • 写锁和写锁之间互斥
读锁分支1

那么 Redisson 为了实现读写锁的这三个特性,一定要有一个标识,来表明当前加的锁是读锁还是写锁对吧,这样才可以继续来判断是否要进行互斥操作

image-20240307204455302

在读锁加锁的第一个分支中,可以看到先去获取 KEYS[1] 中的 mode 属性值了,这个属性值就存储了当前加的锁是读锁还是写锁

如果发现 mode == false 的话,说明没有上锁,当前客户端就可以成功上锁

这里在客户端上锁时,设置了 3 个属性值,加锁之后,在 Redis 中存储的锁信息如下图:

image-20240307205256723

  • hset KEYS[1] mode read) :目的是表明当前锁是写锁还是读锁

  • hset KEYS[1] ARGV[2] 1):将当前客户端线程的 lockName:UUID+threadId 设置到锁的哈希结构中,value 是该锁的重入次数

  • set KEYS[2] .. ':1' 1):设置一个键值对,用来 标识每一把锁的超时时间 。这里的 .. 在 lua 脚本中的含义是进行字符串拼接,因此 key 是 "KEYS[2]:1" ,即 {lockName}:UUID:threadId:{number},这里的 number 表示是重入的第几把锁,如果是第二次重入的锁,number 就为 2

    这里为什么要设置这个键值对呢,是为了避免在锁释放之后,读写锁的超时时间和读锁的超时时间出现不一致,具体解释可以看后边的 【键值对对每个锁的超时时间标识】

读锁分支2

接下来说一下读锁加锁的【分支2】

image-20240307213501880

在【分支1】中如果发现锁并没有被其他客户端线程获取的话,就会直接上锁

如果锁已经被其他客户端线程获取时,此时有两种情况:

  • 其他客户端线程获取的是读锁,读锁之间不互斥,因此可以获取锁
  • 其他客户端获取的锁是写锁,并且发现当前客户端线程的信息在锁的哈希结构中存在,说明是发生了写锁重入

那么在这两种情况下都可以获取锁,在这个【分支 2】中就先通过 hincrby 来对锁的重入次数 +1,并且通过 set 命令将当前线程的信息设置到 Redis 中

这里 set 命令是存储了 key,1 这个键值对这个 key 其实就是 KEYS[2] .. ':' .. ind ,在 lua 脚本中 .. 的含义就是进行字符串拼接,因此这里的 key 就是 KEYS[2]:ind ,这里 ind 的含义是当前锁是重入的第几次,比如是第二次重入,那么 ind = 2

可以看到,整个读锁加锁的流程是比较简单的,主要是了解一下这里对于读锁加锁有在 Redis 中存储的信息

这里再啰嗦一下 Redis 中存储的锁信息为:

  • 读写锁的哈希结构:包括了读写锁的类型、每把锁的唯一标识以及重入次数
  • 键值对:主要作用就是标识每一把读锁的超时时间

根据实际加锁案例来学习读锁加锁的流程

那么假设有一个客户端先来 加读锁 ,那么加锁之后,Redis 中存储的锁信息如下:

image-20240307205256723

可以看到在 KEYS[1] 这个哈希结构中,ARGV[2] 这个键值对主要来标识当前读锁的重入次数

而下边的 KEYS[2]:1 这个键值对则主要来对每一个锁的超时时间进行标识,通过这个键值对就可以获取不同读锁的剩余存活时间

上边说了一个客户端来加锁的情况,假设此时有另一个客户端 B 来加读锁,那么此时 Redis 中的锁结构将变为:

image-20240307211603772

可以看到这里客户端 A 和 B 的分布式锁的 UUID 不同,因此在读写锁 11_come 的哈希结构中出现了两把锁的标识

那么如果此时客户端 A 再次加锁,也就是发生同一把锁的重入,此时 Redis 的锁结构将变为:

image-20240307212925214

可以看到当发生 锁重入 的时候,会先在锁的哈希结构中,将锁的重入次数 + 1

并且新增加一个键值对 "{11_come}:23d25595-6532-4105-a24b-98fea0997bc5:1:rwlock_timeout:2"(客户端A的KEYS[2]:2): "1" ,这里是客户端 A 重入的

来表示重入的这把锁的超时时间

在 Redisson 中,哈希结构中的键值对主要用来存储每把锁的重入次数,而外边的键值对主要用来存储每把锁的超时时间

那么到了这里应该对读锁的加锁流程,以及在 Redis 中锁信息的存储结构就有比较清楚的了解了

为什么要对每个锁的超时时间进行标识?

这里再说一下为什么要给每一个客户端线程都创建一个键值对来标识每个锁的超时时间,如下:

image-20240307222630121

这个键值对的超时时间其实只有在读锁释放的时候才使用得到,接下来举一个例子:

比如说客户端 A 创建一把读锁,此时读锁超时时间默认为 30s,此时读写锁 11_come 的超时时间为 30s,并且客户端 A 创建的这把读锁的超时时间也为 30s,如下:

image-20240308093846494

假设此时客户端 B 在 5s 后来创建一把读锁,此时会将读写锁 11_come 的超时时间重置为 30s,并且客户端 B 创建的这把读锁的超时时间也为 30s,由于 B 在 5s 后来加锁了,因此客户端 A 的锁此时超时时间为 25s

image-20240308094104425

那么假如客户端 B 的读锁释放了,此时只剩下客户端 A 的读锁,此时客户端 A 的读锁超时时间为 25s,而读写锁 11_come 的超时时间为 30s,这显然不合理,如下:

image-20240308094120996

因此 Redisson 考虑到了这种情况,那么为了保证在锁释放之后,可以让 读写锁 11_come每一把读锁的超时时间 保持一直,因此需要记录下来每一把读锁的超时时间,也就是通过 "{11_come}:23d25595-6532-4105-a24b-98fea0997bc5:1:rwlock_timeout:1" 这个 key 来记录

因此在客户端 A 的读锁释放的时候,会去将读写锁 11_come 的超时时间设置为客户端 A 的读锁超时时间,即 25s

image-20240308094429830

读锁的锁续期

Redisson 的读锁 重写了锁续期 的方法,如下:

image-20240308102910476

这里先说一下 lua 脚本中参数的含义:

  • KEYS[1] :读写锁的名称,即 11_come
  • KEYS[2] :key 的前缀,即 {11_come}
  • ARGV[1] :锁超时时间,默认 30s
  • ARGV[2] :当前客户端线程的唯一标识,UUID+threadId,即 9f459868-d210-43c6-afb5-d1bce1fa3472:1

在这个锁续期方法中,其实就是对 读写锁 以及 每一把读锁 进行续期,整个流程并不算复杂,通过看注释应该就可以理解

读锁的解锁流程

读锁的解锁就不打算细说了,如果加锁看明白了,读锁也是不太难的,流程就是减少重入次数,如果重入次数为 0 的话,就将读写锁的哈希结构删除掉即可

不过这里要说一下读锁在解锁时,会使用 publish 发布一条消息,这里说一下发布的这个消息有什么作用

其实不只是在读锁解锁会 publish 消息,在公平锁、可重入锁解锁时都会发布,这里发布消息的目的就是让其他的客户端线程可以知道锁已经被释放了,避免等待较长的时间

发布消息

首先,看一下在读锁解锁时,如何发布消息:

image-20240308103639795

可以看到,在解锁的两个地方发布了消息:

  • 第一个地方:发现 mode == false ,也就是锁已经被释放了,就发布一条通知
  • 第二个地方:解锁成功,在删除锁的哈希结构后,通过 publish 发布一条通知

这里发布的通知的命令是:publish [channel] [message]

channel 的值是 KEYS[2] ,即 redisson_rwlock:{11_come} ,redisson_rwlock 是固定前缀,{11_come} 就是我们这把读写锁的名称,因此这里发布的消息是放在这个通道中了

message 的值是 ARGV[1] ,即 LockPubSub.UNLOCK_MESSAGE ,其实就是一个 Long 值,值为 0

订阅消息

那么既然发布了通知,肯定就要订阅通知,上边已经说了,这个通知的作用就是在线程加锁失败的时候,会进入等待,那么在等待之前会先去这个通道中订阅消息,这样在其他锁被释放之后,等待锁的线程就可以收到消息通知,直接去获取锁,就不需要一直等待了

那么订阅通知的地方就在 RedissonLock 的 lock 方法中:

image-20240308105104434

当发现锁获取失败之后,先订阅通道,再去 while 循环中等待获取锁!

至此读写锁中,读锁的加锁、锁续期、解锁就已经说完了,写锁相对于读锁来讲比较简单一些,都是重复性的内容,这里就不再赘述!

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

掘根教你拿捏C++异常(try,catch,throw,栈解退,异常规范,异常的重新抛出)

在介绍异常之前,我觉得很有必要带大家了解一下运行时错误和c异常出现之前的处理运行时错误的方式。这样子能更深入的了解异常的作用和工作原理 运行阶段错误 我们知道,程序有时候会遇到运行阶段错误,导致程序无法正常运行下去 C在运行时可…

云计算 3月8号 (wordpress的搭建)

项目wordpress 实验目的: 熟悉yum和编译安装操作 锻炼关联性思维,便于以后做项目 nginx 编译安装 1、安装源码包 [rootlinux-server ~]# yum -y install gcc make zlib-devel pcre pcre-devel openssl-devel [rootlinux-server ~]# wget http://nginx.…

玩转Random的正确姿势

一、关于java.util.Random 我们知道,在数学领域里面0到1之间的小数是无穷无尽的,所以如果从数学角度上来讲,要计算0到1之间某个小数出现的概率是不现实的,但是作为计算机领域的人员应该会注意到,大多数编程语言中随机…

类与对象(三)--static成员、友元

文章目录 1.static成员1.1概念🎧面试题✒️1.2static的特性🎧1.3思考🎧 2.友元2.1什么是友元?🎧2.2两种友元关系:🎧 1.static成员 1.1概念🎧 🔎 static关键字用于声明类…

CSP-J 2021 T1 分糖果

文章目录 题目传送门算法解析总代码提交记录尾声 题目传送门 洛谷 P7909 [CSP-J 2021] 分糖果 算法解析 首先简化一下题目:取一个值 k k k,使 L ≤ k ≤ R L \leq k \leq R L≤k≤R,输出最大的 k % n k \% n k%n 一个数 % n \% n %n…

Selenium常见元素操作,学完就能上手

web端自动化测试在回归测试、兼容测试扮演着举足轻重的角色,作为web自动化测试工程师,日常工作主要的部分就是编写自动化测试用例代码,借助的开源框架来说,目前市场占有率较高的仍然是selenium。 如何使用selenium完成web页面元素…

测试用例的一些设计好方法,学会就是赚到!

一、概述 测试用例设计方法是指根据测试目的和测试对象,选择合适的技术和策略,来生成测试用例的方法。 测试用例是指导测试过程的重要文档,主要包括用例编号、测试目的、测试步骤、预期结果等。测试用例的编写需要遵循一些原则,…

Deepl翻译相关介绍

DeepL是一种机器翻译软件,它在2017年首次发布。该软件利用了神经网络和深度学习技术,以提供更准确和自然的翻译结果。DeepL的翻译质量被广泛认为是当前机器翻译技术中最佳的之一。 官网:DeepL翻译:全世界最准确的翻译 DeepL具有许…

离散数学——特殊图思维导图

离散数学——特殊图思维导图 目录 前言 内容 大纲 参考 前言 这是当初学习离散数学时整理的笔记大纲,其中包含了自己对于一些知识点的体悟。现将其放在这里作为备份,也希望能够对你有所帮助。 当初记录这些笔记只是为了在复习时更快地找到对应的知…

自研cloud框架专题–web模块(三)

项目特点一:框架集成 1.引入核心依赖2.配置相关功能 二:功能介绍 1.swagger支持并提供swagger快速配置2.knife增强swagger支持3.全局请求参数校验(Validation)支持4.字段脱敏支持5.默认jackson序列化6.xss,cors支持7.访问日志支持8.全局异常处理,统一返回结果9.系统关键及常用信…

程序运行的基本流程

操作系统(应用程序): 装系统就是将操作系统安装到硬盘1中 计算机启动的基本过程: 总结: 程序一般保存在硬盘中,软件安装的过程就是将程序写入硬盘的过程程序在运行时会加载进入内存,然后由CPU…

【Docker】若依ruoyi项目部署

一 搭建局域网 1 # 搭建net-ry局域网,用于部署若依项目docker network create net-ry --subnet172.68.0.0/16 --gateway172.68.0.1 # 注意1:关闭宿主机的防火墙,否者容器内部的MySQL、redis等服务,外部访问不了;开放…