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)) thenredis.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) thenredis.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/

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

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

相关文章

什么是Google SEO优化,如何做好谷歌seo排名?2024年谷歌搜索引擎优化(谷歌SEO)3分钟速通教程指南

1 - 什么是SEO? 谷歌排名优化(SEO:Search Engine Optimization)是指当您在谷歌搜索那里输入一个您正在推广的产品或服务的关键词时,如何在使您的站在Google里获得一个较高的排名位置而做的优化过程。谷歌排名优化的意…

算法学习笔记(LCA)

L C A LCA LCA:树上两个点的最近公共祖先。(两个节点所有公共祖先中,深度最大的公共祖先) L C A LCA LCA的性质: 在所有公共祖先中, L C A ( x , y ) LCA(x,y) LCA(x,y)到 x x x和 y y y的距离都最短。 x …

什么是AOT,AOT 有什么优点

JDK 9 引入了一种新的编译模式 AOT (Ahead of Time Compilation)。与 JIT (Just-In-Time Compilation) 不同,AOT 在程序执行前将其编译成机器码,属于静态编译。这种模式具有很多优点,但也有一些限制。本文将详细探讨 AOT 的优点以及其限制。 …

六西格玛培训证书攻略2024:一站式解决方案助你快速上手

目前,企业对于员工的专业能力和综合素质要求越来越高。六西格玛作为一种先进的质量管理方法,已经成为众多企业提升运营效率、降低成本的重要手段。张驰咨询针对2024年六西格玛培训证书考取,为广大学员制定了实用的攻略,帮助学员们…

玩转大模型 企业AI着陆新正解 神州问学AI原生赋能平台正式发布

在人工智能技术日新月异的今天,神州数码凭借深厚的行业洞察和技术积累,揭开了AI原生赋能平台——神州问学的神秘面纱。作为企业AI着陆的加速引擎,神州问学致力于通过AI原生场景赋能,为企业开辟一条通往智能未来的坦途。 神州问学—…

vue3使用el-radio-group获取表格数据无法选中问题

这里是引用 今天写项目发现使用el-radio-group无法获取表格中的数据&#xff0c;于是去官网查看了一下&#xff0c;发现写的没啥问题&#xff0c;就是 <el-radio value"1" size"large"> 未知</el-radio>这样的写法&#xff0c;又在网上看了一些…

不写一行代码,使用ChatGpt开发一个射击游戏

1.简介 最近需要开发一个网页应用&#xff0c;想到了使用ChatGpt生成Html页面&#xff0c;生成的效果非常好&#xff0c;说几句话就可以实现复杂的功能。不过需要一步步耐心的引导。然后我就想到可以用ChatGpt生成一个网页游戏。这个游戏包含了人物移动、跳跃、射击、生命值&a…

02Django项目安装和环境变量设置

凯哥英语视频 Django项目安装和环境变量设置 凯哥英语视频1.汉化1.打开PyCharm&#xff0c;点击File&#xff0c;再点击Settings2.然后点击 Plugins&#xff0c;再Marketplace&#xff0c;找到Chinese&#xff08;simplified&#xff09;Language&#xff0c;再点击Install然后…

linux给history查看历史执行命令加上日期和时间

问题&#xff1a; 打开linux&#xff0c;执行history看一下上次执行的命令&#xff0c;结果命令是显示出来了&#xff0c;但没有日期和时间。 解决办法&#xff1a; 编辑 ~/.bashrc vim ~/.bashrc 在末尾加入以下命令 export HISTTIMEFORMAT"%F %T " 保存并退…

Rust语言实现图像编码转换

一、概述 Rust 作为一门现代的系统编程语言&#xff0c;不仅性能出色&#xff0c;安全性高&#xff0c;而且生态系统也在不断成熟。在图像处理方面&#xff0c;image-rs库是 Rust 社区中广泛使用的一个开源库&#xff0c;它提供了丰富的图像编解码功能。今天&#xff0c;我将带…

GPT-4o: 从最难的“大海捞针”基准看起

大模型技术论文不断&#xff0c;每个月总会新增上千篇。本专栏精选论文重点解读&#xff0c;主题还是围绕着行业实践和工程量产。若在阅读过程中有些知识点存在盲区&#xff0c;可以回到如何优雅的谈论大模型重新阅读。另外斯坦福2024人工智能报告解读为通识性读物。若对于如果…

Python slice() 使用方法及示例说明

slice()参数 slice() 可以采用三个参数&#xff1a; start&#xff08;可选&#xff09; -对象切片开始的起始整数。如果未提供或者值为None&#xff0c;则默认为第一个数据。 stop-整数&#xff0c;直到切片发生。切片在索引stop-1&#xff08;最后一个元素&#xff09;处停…

基于EBAZ4205矿板的图像处理:12二值化图像的膨胀与腐蚀

基于EBAZ4205矿板的图像处理&#xff1a;12二值化图像的膨胀与腐蚀 先看效果 注意&#xff1a;我的项目中的膨胀和腐蚀是对二值化图像中的像素值为255的像素进行处理&#xff0c;而我的图像中255为白色&#xff0c;0为黑色&#xff0c;所以是对颜色为白色的像素点进行的膨胀和…

【Vue】Vue 中的数据传递策略:探索跨组件通信的多样化方法

Vue 中的数据传递策略&#xff1a;探索跨组件通信的多样化方法 在现代的前端开发过程中&#xff0c;Vue.js 以其灵活和易于理解的结构脱颖而出&#xff0c;成为了广受欢迎的 JavaScript 框架之一。在构建动态应用时&#xff0c;组件之间的数据传递是必不可少的&#xff0c;但随…

Google I/O 2024:有关AI的一切已公布|TodayAI

2024年谷歌I/O大会圆满落幕&#xff0c;谷歌在会上发布了一系列更新&#xff0c;涵盖从最新的人工智能技术到Android系统的多项改进。此次大会特别关注于谷歌的Gemini人工智能模型&#xff0c;并详细介绍了这些模型如何被融入到Workspace、Chrome等多个应用程序中&#xff0c;展…

男士内裤哪个牌子质量好又舒服?五款不容错过的男士内裤

男士内裤&#xff0c;作为男士日常穿着的重要贴身衣物&#xff0c;其舒适度和透气性至关重要。尽管有些男士可能习惯长时间穿着同一条内裤&#xff0c;但为了确保健康和舒适&#xff0c;建议每3-6个月更换一次内裤。长时间不更换内裤会导致其舒适性和透气性下降&#xff0c;同时…

性价比王者HUSB237,极简PD Sink的“瘦身秘籍”

在小型化、高集成的要求下&#xff0c;慧能泰取电芯片进行技术升级后“瘦身成功”&#xff0c;推出最新一代极具性价比的最简PD Sink取电芯片——HUSB237。 图1&#xff1a;HUSB237 demo及封装图 HUSB237 是一款极具性价比的最简PD Sink取电芯片&#xff0c;支持PD3.1协议包含…

C#知识|上位机面向对象编程时如何确定类?

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 01 项目分类 1.1、无数据库的项目&#xff1a;应用面向对象的思想和发方法设计&#xff0c;完成各个类的设计过程&#xff0c;确定各个类之间的关系。 1.2、有数据库的项目&#xff1a;项目的框架和思路相对固定&…

【风电功率预测-粉丝福利】向量加权平均算法优化卷积神经网络结合长短记忆网络INFO-CNN-LSTM

如何做 风电功率预测是一项重要的任务&#xff0c;可以帮助优化风电发电效率和电网调度。为了提高风电功率预测的准确性&#xff0c;可以使用卷积神经网络&#xff08;Convolutional Neural Network, CNN&#xff09;结合长短期记忆网络&#xff08;Long Short-Term Memory, L…

管道通信机制详解:无名管道 vs 有名管道

目录 无名管道&#xff08;匿名管道&#xff09; 定义 特点 创建与使用 有名管道&#xff08;FIFO&#xff09; 定义 特点 创建与使用 总结 在多进程通信中&#xff0c;管道是一种非常基本且实用的机制&#xff0c;它允许进程间进行数据传输。根据是否在文件系统中有名称&#…