Redisson中分布式锁的实现原理

redisson版本:3.27.2

简介

锁归根结底就是对同一资源的竞争抢夺,不管是在单体的应用亦或者集群的服务中,上锁都是对同一资源进行修改的操作。
至于分布式锁,那就是多个服务器或资源,同时抢占某一单体应用的同个资源了。在本篇文章中,抢占的资源就是Redis中的某个Key了。

原理

上锁

RLock lock = RedissonClient.getLock("test-lock");
lock.lock();

执行lock.lock()后,最终会在Redis中执行一段lua脚本。来判断锁是否已经被占用:

if ((redis.call('exists', KEYS[1]) == 0) 
    or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end ;
return redis.call('pttl', KEYS[1]);

参数:

名称内容
KEY[1]锁名称
ARGV[1]锁过期时间,毫秒
ARGV[2]锁对象ID+当前线程ID

注意,在lua脚本中,上锁时并非设置一个key-value,而是使用了hash结构。

redis.call(‘hincrby’, KEYS[1], ARGV[2], 1);

这样做的目的是Redisson不光实现了分布式锁,还增加了一个特性:可重入。因为单独的键值对无法存储上锁次数,就使用了hash结构。


上锁时redis日志:

10:40:50.064 [0 192.168.65.1:34743] "EVAL" "if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);" "1" "test-lock" "30000" "fcce544e-09e1-48bb-9c90-2c77c75d673f:1"
10:40:50.064 [0 lua] "exists" "test-lock"
10:40:50.064 [0 lua] "hincrby" "test-lock" "fcce544e-09e1-48bb-9c90-2c77c75d673f:1" "1"
10:40:50.064 [0 lua] "pexpire" "test-lock" "30000"

可以很清晰的从日志分析出来,Redisson在给分布式锁上锁时所做的操作。

判断锁是否被占->没有被占,抢占并设置过期时间

那么在第一次抢占不到锁时,Redisson在等待时,会不会做些其他事情呢?
的确,Redisson在等待锁时,还会做一些其他事情,免得在傻傻等待。

等待锁释放

在抢不到锁的时候,Redisson会监听redisson_lock__channel开头的Channel
锁释放时,抢占锁的应用会向这个Channel发布一个消息(消息内容为:0)。向正在等待锁释放的应用通知此时锁已经释放了,可以尝试抢占锁了。
在上面的这个例子中,对应的Channel名称为:redisson_lock__channel:{test-method},消息内容为:0。

解锁

在抢到锁并且本地逻辑运行完成后,此时就需要解锁让其他应用运行下去了。

RLock lock = RedissonClient.getLock("test-lock");
lock.lock();

执行的lua脚本

local val = redis.call('get', KEYS[3]); 
if val ~= false then 
  return tonumber(val);
end;
-- 看hash中是否还存在这个键(RLock对象的名称以及线程名称)
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then 
  return nil;
end;
-- counter:hash中减一后的值
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
if (counter > 0) then 
  redis.call('pexpire', KEYS[1], ARGV[2]); 
  redis.call('set', KEYS[3], 0, 'px', ARGV[5]); 
  return 0; 
-- 为0了,说明重入锁的次数都删掉了
else 
  -- 删除锁对应的redis hash表
  redis.call('del', KEYS[1]);
  -- 发布当前锁释放的通知
  redis.call(ARGV[4], KEYS[2], ARGV[1]); 
  -- 设置对象对应的 值为1 
  redis.call('set', KEYS[3], 1, 'px', ARGV[5]); 
  return 1; 
end;

参数:

参数名说明
KEY[1]分布式锁名称test-lock
KEY[2]redis pub/sub 通道名称redisson_lock__channel:{test-lock}
KEY[3]正在解锁操作标识redisson_unlock_latch:{test-lock}:96ca7c366fa0a6bda6d39931f2092eb1
ARGV[1]pub/sub 通道值-解锁消息(0)
ARGV[2]锁过期时间
ARGV[3]Lock对象对应的锁名称d5804b0b-50e4-4d61-a91a-319c2ddb5b1d:1
ARGV[4]PUBLISH
ARGV[5]正在解锁操作标识KEY对应过期时间

解锁时Redis日志:

10:40:50.073 [0 192.168.65.1:34745] "EVAL" "local val = redis.call('get', KEYS[3]); if val ~= false then return tonumber(val);end; if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); redis.call('set', KEYS[3], 0, 'px', ARGV[5]); return 0; else redis.call('del', KEYS[1]); redis.call(ARGV[4], KEYS[2], ARGV[1]); redis.call('set', KEYS[3], 1, 'px', ARGV[5]); return 1; end; " "3" "test-lock" "Redisson_lock__channel:{test-lock}" "Redisson_unlock_latch:{test-lock}:4673449b09ccae99bad2a89c9f0122de" "0" "30000" "fcce544e-09e1-48bb-9c90-2c77c75d673f:1" "PUBLISH" "13500"
10:40:50.073 [0 lua] "get" "Redisson_unlock_latch:{test-lock}:4673449b09ccae99bad2a89c9f0122de"
10:40:50.073 [0 lua] "hexists" "test-lock" "fcce544e-09e1-48bb-9c90-2c77c75d673f:1"
10:40:50.073 [0 lua] "hincrby" "test-lock" "fcce544e-09e1-48bb-9c90-2c77c75d673f:1" "-1"
10:40:50.073 [0 lua] "del" "test-lock"
10:40:50.073 [0 lua] "PUBLISH" "Redisson_lock__channel:{test-lock}" "0"
10:40:50.073 [0 lua] "set" "Redisson_unlock_latch:{test-lock}:4673449b09ccae99bad2a89c9f0122de" "1" "px" "13500"
10:40:50.076 [0 192.168.65.1:34746] "DEL" "Redisson_unlock_latch:{test-lock}:4673449b09ccae99bad2a89c9f0122de"

解锁的lua脚本比上锁时的脚本有太多的逻辑了,不过还是分为了三块:

  1. 判断是否有其他线程在解锁,如果有其他线程在同时释放锁时,忽略本次操作
local val = redis.call('get', KEYS[3]); 
if val ~= false then 
  return tonumber(val);
end;
  1. 判断锁是否已经释放,锁已经释放了,无需操作
-- 看hash中是否还存在这个键(RLock对象的名称以及线程名称)
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then 
  return nil;
end;
  1. 给锁的hash结构减一,根据减一后的结果做进一步处理。
-- counter:hash中减一后的值
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
if (counter > 0) then 
  redis.call('pexpire', KEYS[1], ARGV[2]); 
  redis.call('set', KEYS[3], 0, 'px', ARGV[5]); 
  return 0; 
-- 为0了,说明重入锁的次数都删掉了
else 
  -- 删除锁对应的redis hash表
  redis.call('del', KEYS[1]);
  -- 发布当前锁释放的通知
  redis.call(ARGV[4], KEYS[2], ARGV[1]); 
  -- 设置对象对应的 值为1 
  redis.call('set', KEYS[3], 1, 'px', ARGV[5]); 
  return 1; 
end;

解锁时,会发布一条消息,通知锁已经释放。

10:40:50.073 [0 lua] “PUBLISH” “redisson_lock__channel:{test-lock}” “0”

方便其他正在等待锁的Redisson应用及时唤醒抢占锁。

其他隐藏配置

Redisson在默认上锁时设置的锁过期时间为30S,与其他Java Redis库不设置过期时间的逻辑相反。
由于Redisson显示声明了锁过期时间,那么他一定会在别的地方去一直延长该时间,否则锁在用着用着就被别人抢占了,
于是Redisson中一个特殊机制就出现了:看门狗机制
至于为什么Redisson要这么做,在他对于这个看门狗过期时间配置项可以得知:

This prevents against infinity locked locks due to Redisson client crash or any other reason when lock can’t be released in proper way.
这可以防止由于Redisson客户端崩溃或任何其他原因导致无法以适当的方式解锁而导致的无限锁定。

image.png
看门狗续期脚本:
如果锁还在使用中,那么重置锁的过期时间,否则不做任何操作。

if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 1;
end ;
return 0;
名称内容
KEY[1]锁名称test-lock
ARGV[1]锁过期时间,毫秒30000
ARGV[2]锁对象ID+当前线程IDd5804b0b-50e4-4d61-a91a-319c2ddb5b1d:1

引用文章

https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers
https://github.com/redisson/redisson/wiki/2.-Configuration/

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Springboot+Vue的Java项目-农产品直卖平台系统开发实战(附演示视频+源码+LW)

大家好!我是程序员一帆,感谢您阅读本文,欢迎一键三连哦。 💞当前专栏:Java毕业设计 精彩专栏推荐👇🏻👇🏻👇🏻 🎀 Python毕业设计 &am…

AD软件针对分散的元器件归一排列

先框选 然后快捷键TOL 绿色的十字箭头选框选位置 完成

将excel表中的数据导入到navicat中

1.将excel中的表头改成英文 2.在navicat中右键表,选择【导入向导】 3.在弹出的导入向导中选择Excel文件,然后点击【下一步】 4.选择需要导入的excel,选中后,在下方会罗列出excel中的sheet,勾选需要导入的sheet&#xf…

Chromium 调试指南2024 Windows11篇-使用日志来辅助调试(八)

1. 日志:你的第一个调试工具 日志是开发者最简单也是最常用的调试工具之一,它能够提供程序运行时的详细记录。通过合理的日志记录策略,开发者可以快速定位问题发生的上下文,理解程序的运行流程和状态。 2. 如何在Chromium中使用…

QMT如何写代码获取基金数据?方法总结!

此函数被设计为只支持单一基金查询,用于获取详细的股票信息。该函数可以让您接收关于特定基金的深度信息,包括但不限于其涨跌停价格、上市日期、退市日期以及期权到期日等重要数据。这将为您提供详尽的信息,以便更好地理解并分析股票的历史和…

【vue2项目经验总结:a标签干扰路由】

当我们点击页面中的a标签实现跳转时,会发现网页上方的路由也切换成了a标签的id值: 刷新后页面也会变成空白: 解决方法: 添加Click方法,传入的参数与id值保持一致 scrollIntoView() 方法,将该元素滚动到…

pycharm连接远程服务器,解决终端出现乱码问题

在终端输入命令时会有乱码问题,是字体编码设置错误。 根据上述步骤,设置完成后重启就可以了。

C#语言进阶

一、简单数据结构类 1. ArrayList ArrayList是一个 C# 为我们封装好的类,它的本质是一个 object 类型的数组。ArrayList类帮助我们实现了很多方法,比如数组的增删查改 1.1 声明 using System.Collections;ArrayList array new ArrayList(); 1.2 增…

MyBatis的创建和测试

创建项目点击Spring Initializr然后点击next 点击SQL 选择里面的Mybatis Framework和Mysql Driver 按如下图片创建项目 user表中的数据 #下面这些内容是为了让MyBatis映射 #指定Mybatis的Mapper文件 mybatis.mapper-locationsclasspath:mappers/*xml #指定Mybatis的实体目录 my…

亿级流量系统架构设计与实战

💂 个人网站:【 摸鱼游戏】【神级代码资源网站】【工具大全】🤟 一站式轻松构建小程序、Web网站、移动应用:👉注册地址🤟 基于Web端打造的:👉轻量化工具创作平台💅 想寻找共同学习交…

强烈推荐的AI生成PPT软件,快捷高效

提起PPT,大家的第一反应就是痛苦。经常接触PPT的学生党和打工人,光看到这3个字母,就已经开始头痛了: 1、PPT内容框架与文案挑战重重,任务艰巨,耗费大量精力。 2、PPT的排版技能要求高,并非易事…

EmotiVoice 实时语音合成TTS;api接口远程调用

参考:https://github.com/netease-youdao/EmotiVoice 测试整体速度可以 docker安装: 运行容器:默认运行了两个服务,8501 一个streamlit页面,另外8000是一个api接口服务 docker run -dp 8501:8501 -p 8250:8000 syq163/emoti-voice:latest##gpu运行 (gpu运行遇到CUDA er…

高效且安全的传输工具:FileLink跨网文件传输

在数字化时代,文件传输已成为我们日常工作和生活不可或缺的一部分。无论是企业内部的资料共享,还是企业对外的文件交换,都需要一个高效、稳定且安全的传输工具。而FileLink跨网文件传输正是满足这些需求的理想选择。 FileLink跨网文件传输 首…

【环境安装】nodejs 国内源下载与安装以及 npm 国内源配置

前言 Node.js 是一个基于 Chrome V8 引擎构建的 JavaScript 运行时环境,它能够使 JavaScript 在服务器端运行。它拥有强大的包管理器 npm,使开发者能够轻松管理和共享 JavaScript 代码包。 在中国,由于众所周知的原因,我们可能会…

Spring,SpringMVC,SpringBoot知识总结

1.简述Spring,SpringMVC,SpringBoot各自特点及联系 Spring、Spring MVC 和 Spring Boot 是 Java 开发中常用的三个框架,它们之间有以下关系: Spring:是一个全功能的 JavaEE 应用程序框架。它提供了一系列的解决方案&#xff0c…

VUE2+ffmpeg处理非h264编码格式视频

1、安装npm install ffmpeg/ffmpeg0.10.0 ffmpeg/core0.9.8 video.js8.12.0 2、在vue.config.js中devServer配置 headers: {// 如果需要用到ffmpeg确保ShareArrayBuffer能够正常使用,可能会有安全隐患Cross-Origin-Embedder-Policy: require-corp,Cross-Origin-Opener-Policy:…

XM1553B 航电总线协议模块(内置总线收发器)

是一款4M速率的高性能1553B模块,兼容1Mbps通信速率,支持单功能(BC,orRT,or BM)和多功能(BC&1RT&BM),该模块内部集成32K16bit的双端口RAM和4M 1553B收发器。 主机端接口支持串口和SPI。串…

问题—前端调用接口url多加一个/,本地可以调通,测试环境报错302,分开调两个接口

问题背景 接口url前面多加一个/ ,npm run serve 起项目,本地调用正常 npm run build 打包到测试环境,接口出现问题,分开调用接口,且报302错误 问题原因: 本地开发环境和测试环境的URL处理方式不同 本地使…

Pythonz中 SortedList的用法

文章目录 安装 sortedcontainers 库SortedList 基本用法特性与操作更多操作性能考虑实例:范围查询与交集高级特性与最佳实践自定义比较函数并行处理与多线程性能调优与其他数据结构结合使用 应用案例1. 金融交易记录分析2. 日志文件管理3. 学生成绩管理系统4. 实时数…

计算机Java项目|Springboot学生读书笔记共享

作者主页:编程指南针 作者简介:Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容:Java项目、Python项目、前端项目、人工智能与大数据、简…