redis面试(十三)公平锁排队代码剖析
我们来看一下第二种redis分布式锁
第一种锁是可重入锁,非公平可重入锁,所谓的非公平可重入锁是什么意思呢?胡乱的争抢,根本没有任何公平性和顺序性可言
第二种锁,可重入锁,公平锁
通过公平锁,可以保证,客户端获取锁的顺序,就跟他们请求获取锁的顺序,是一样的,公平锁,排队,谁先申请获取这把锁,谁就可以先获取到这把锁,这个是按照顺序来的
会把各个客户端对加锁的请求进行排队处理,保证说先申请获取锁的,就先可以得到这把锁,实现所谓的公平性
可重入非公平锁、公平锁,他们在整体的技术实现上都是一样的,只不过唯一不同的一点就是在于加锁的逻辑那里
RLock fairLock = redisson.getFairLock("anyLock");
fairLock.lock();
fairLock.unlock();
这个代码就是获取公平锁的方法。
RedissonFairLock是RedissonLock的子类,整体的锁的技术框架的实现,都是跟之前讲解的RedissonLock是一样的,无非就是重载了一些方法,加锁和释放锁的lua脚本的逻辑稍微复杂了一些,别的没什么特别的
第一个线程第一次加锁
我们来分析一下这个加锁的lua脚本
if (command == RedisCommands.EVAL_LONG) {return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,// remove stale threads"while true do "+ "local firstThreadId2 = redis.call('lindex', KEYS[2], 0);"+ "if firstThreadId2 == false then "+ "break;"+ "end; "+ "local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));"+ "if timeout <= tonumber(ARGV[4]) then "+ "redis.call('zrem', KEYS[3], firstThreadId2); "+ "redis.call('lpop', KEYS[2]); "+ "else "+ "break;"+ "end; "+ "end;"+ "if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) "+ "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then " +"redis.call('lpop', KEYS[2]); " +"redis.call('zrem', KEYS[3], ARGV[2]); " +"redis.call('hset', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"if (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; " +"local firstThreadId = redis.call('lindex', KEYS[2], 0); " +"local ttl; " + "if firstThreadId ~= false and firstThreadId ~= ARGV[2] then " + "ttl = tonumber(redis.call('zscore', KEYS[3], firstThreadId)) - tonumber(ARGV[4]);" + "else "+ "ttl = redis.call('pttl', KEYS[1]);" + "end; " + "local timeout = ttl + tonumber(ARGV[3]);" + "if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then " +"redis.call('rpush', KEYS[2], ARGV[2]);" +"end; " +"return ttl;", Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName), internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime);
}
首先,第一行while true do 进入一个while的死循环
第二行local firstThreadId2 = redis.call(‘lindex’, KEYS[2], 0);
先看一下KEYS[2]这个参数是什么,也就是这部分lua脚本下面那个List里面第二个参数,第一个是getName(),不用想肯定是和我们传的“anyLock”有关,那第二个KEYS[2] = threadsQueueName = redisson_lock_queue:{anyLock},基于redis的数据结构实现的一个队列,第三个KEYS[3] = timeoutSetName = redisson_lock_timeout:{anyLock} 基于redis的数据结构实现的一个Set数据集合,有序集合,可以自动按照你给每个数据指定的一个分数(score)来进行排序
ARGV = internalLockLeaseTime, getLockName(threadId), currentTime
ARGV[1] = 30000毫秒
ARGV[2] = UUID:threadId 与线程有关
ARGV[3] = 当前时间(10:00:00) + 5000毫秒 = 10:00:05
ARGV[4] = 当前时间(10:00:00)
再回到lua脚本 local firstThreadId2 = redis.call(‘lindex’, KEYS[2], 0);
lindex 命令用于通过索引获取列表中的元素。也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素
那这行的意思就是从名为redisson_lock_queue:{anyLock} 的队列数组中弹出来下标为0的元素,也就是队列中的第一个元素
下一行,如果不存在的话,直接跳出while循环
if firstThreadId2 == false then "+ "break;"
+ "end;
那我们第一次加锁的时候,肯定是不存在的,所以往下看其他逻辑
这里有几个判断,第一个exists anyLock 这个锁是否存在,不存在,返回true
第二个和第三个是or
第二个exists redisson_lock_queue:{anyLock},队列是否存在,不存在,返回true
第三个lindex redisson_lock_queue:{anyLock} 弹出第一个元素,是否等于 UUID:threadId 这个是要返回false,但是第二和第三个判断 是or,所以第二第三只要有一个true就成立了
if (redis.call('exists', KEYS[1]) == 0) and ((redis.call('exists', KEYS[2]) == 0) "+ "or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then
那继续往下走
lpop redisson_lock_queue:{anyLock},弹出队列的第一个元素,现在队列是空的,所以什么都不会干
zrem redisson_lock_timeout:{anyLock} UUID:threadId,从set集合中删除threadId对应的元素,此时因为这个set集合是空的,所以什么都不会干
hset anyLock UUID:threadId 1,加锁,这和之前的加锁逻辑一样,加一个名字为anyLock的map结构,键值对key:value 为“UUID:threadId”: 1
redis.call(‘pexpire’, KEYS[1], ARGV[1]); 给这个锁设置过期时间,默认30s
返回一个nil,在外层代码中,就会认为是加锁成功,此时就会开启一个watchdog看门狗定时调度的程序,每隔10秒判断一下,当前这个线程是否还对这个锁key持有着锁,如果是,则刷新锁key的生存时间为30000毫秒
这就是公平锁的加锁原理
第二个线程第一次加锁
那这是第一次加锁,后面是怎么实现公平锁? 再来看一下
第二个线程来尝试加锁,首先也是进入while true死循环,lindex redisson_lock_queue:{anyLock} 0,获取队列的第一个元素,此时队列还是空的,所以获取到的是false,直接退出while true死循环
再次进入这个判断,这次就有些不一样了
‘exists’, anyLock == 0 此时anyLock锁已经存在了,所以这个条件肯定就不成立了
那进行下面的判断
if (redis.call(‘hexists’, KEYS[1], ARGV[2]) == 1) then
这个是判断,在名为anyLock这个map锁的键值对中 有没有名为 “UUID-02:threadId-02” 的key,此时肯定也是不成立,因为现在就是这个线程第一次请求加锁的。
再往下就是排队的关键逻辑了,我们来分析一下
local firstThreadId = redis.call(‘lindex’, KEYS[2], 0);
取出来队列中的第一个元素
if firstThreadId ~= false and firstThreadId ~= ARGV[2] then
这是判断取出来的元素不为空,此时不成立
所以else中的逻辑 ttl = redis.call(‘pttl’, KEYS[1]);这个是获取 anyLock这个锁的剩余生存时间,假设是20000毫秒
继续往下local timeout = ttl + tonumber(ARGV[3]); 算出来 ttl + 当前时间 + 5000毫秒是什么时间
比如:当前是2023-01-01 10:00:00, 那么加上20000毫秒,再加 5000毫秒,结果就是10:00:25 的long型时间戳
if redis.call(‘zadd’, KEYS[3], timeout, ARGV[2]) == 1 then
在set有序集合redisson_lock_timeout:{anyLock} 中,新增一个线程是 UUID-02:threadId-02的数据,排序权重是2023-01-01 10:00:25的long型时间戳 ,并且新增成功的话,
rpush’, KEYS[2], ARGV[2]
在队列 redisson_lock_queue:{anyLock} 中也新增一个元素UUID-02:threadId-02的数据
最后返回一个anyLock的存活时间ttl,之前的逻辑还记得吧,如果加锁的时候返回有效期时间的话,也会进入一个while死循环不断地尝试加锁。重新执行lua脚本
后面的线程也是同理,timeout时间戳不断增大,有序集合redisson_lock_timeout:{anyLock} 中会按照这个权重自动排序,队列 redisson_lock_queue:{anyLock} 中也按照放入的顺序往后排。
第三个线程第一次加锁
这次进来这个lua脚本的时候就要进入这个逻辑中了
local firstThreadId2 = redis.call(‘lindex’, KEYS[2], 0); 判断队列中第一个元素是否存在,上面已经放进去了,肯定是存在的,而且这是第二个线程的
local timeout = tonumber(redis.call(‘zscore’, KEYS[3], firstThreadId2));
获取有序队列中,元素UUID-02:threadId-02的权重值。
if timeout <= tonumber(ARGV[4]) then
上面我们说了,这个权重值是2023-01-01 10:00:25的long型时间戳,那这里是判断当前时间的时间戳和这个相比。 意思就是,当前时间是否已经超过了2023-01-01 10:00:25。
这次我们先假设不成立,继续往下
exists’, KEYS[1] == 0 肯定也是不成立,已经存在了,
此时队列中第一个元素是UUID-02:threadId-02
ARGV[2] 是UUID-03:threadId-03
local firstThreadId = redis.call(‘lindex’, KEYS[2], 0);
那这里判断的两个条件成立
firstThreadId不等于空,并且不等于当前线程
if firstThreadId ~= false and firstThreadId ~= ARGV[2] then
这里获取的就是,第一个线程的权重时间戳-当前时间的时间戳,意思是,队列第一个线程还有多久会去竞争锁
然后再拿着这个时间差+当前时间+5s
这样一来,这个线程的权重在有序队列中,肯定是排在第一个线程后面的。
ttl = tonumber(redis.call(‘zscore’, KEYS[3], firstThreadId)) - tonumber(ARGV[4]);
然后就是入队,排队
if redis.call(‘zadd’, KEYS[3], timeout, ARGV[2]) == 1 then
redis.call(‘rpush’, KEYS[2], ARGV[2]);
此时我们看一下情况
如果超过的话,理论上来说anyLock这个锁已经被释放掉了。
那就把元素UUID-02:threadId-02从 有序集合redisson_lock_timeout:{anyLock} 中移除
redis.call(‘zrem’, KEYS[3], firstThreadId2);
队列redisson_lock_queue:{anyLock}中也把第一个元素删除
redis.call(‘lpop’, KEYS[2]);
相关文章:

redis面试(十三)公平锁排队代码剖析
我们来看一下第二种redis分布式锁 第一种锁是可重入锁,非公平可重入锁,所谓的非公平可重入锁是什么意思呢?胡乱的争抢,根本没有任何公平性和顺序性可言 第二种锁,可重入锁,公平锁 通过公平锁,…...
冷热数据拆分
订单系统设计方案之如何做历史订单和归档 订单数据越来越多,数据库越来越慢该怎么办? 随着历史订单不断累积,2017年MySQL中订单表数据量已达千万级。之后的订单数据,远远大于亿级 对数据量大的问题,进行了以下优化…...
JavaScript 基础(四)
五、DOM编程 1.常用事件 onload 页面加载后触发事件 onscroll 滚动时触发 onresize 尺寸变化时 onclick 鼠标点击 onmouseover 鼠标悬停 onmouseout 鼠标移出 onmousemove 鼠标移动,会触发多次 onfocus 对象获得光标(焦点)时&#x…...
《机器学习by周志华》学习笔记-神经网络-01神经元模型
1、背景 本书所谈的「人工神经网络」不是生物学意义的神经网络。这是T.Kohonen 1988年在Neural Networks创刊号上给出的定义。 2、概念 2.1、神经网络 关于「神经网络(neural networks)」的研究很早就已经出现过,今天的「神经网络」已经是一个比较大且多学科交叉的领域,其…...
C#中常用的扩展类
/// <summary>/// 常用扩展/// </summary>public static class UsualExtension{public static string[] chineseNumbers { "零", "一", "二", "三", "四", "五", "六", "七", &…...

麒麟v10(ky10.x86_64)升级——openssl-3.2.2、openssh-9.8p1
系统版本: ky10.x86_64 下载安装包并上传 openssh下载地址 https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable openssl下载地址 https://openssl-library.org/source/index.html zlib下载地址 https://zlib.net/fossils/ 上传安装包 备份配置文件 cp -r /etc/ssh /et…...
【Unity】有限状态机和抽象类多态
一、介绍 有限状态机是一个用来进行对象状态管理的计算模型。它由一组状态、一个或者多个触发事件以及状态之间的转换条件所组成。 对于任意一个游戏对象,我们可以为其编写一个或者多个状态机,使其能够在不同状态下有不同的决策和运作机制。 核心思想…...

KETTLE调用http传输中文参数的问题
场景:检查服务器异常(hive)服务,就通过http发送一条短信到手机上,内容类似:【通知】 S T A R T D A T E h i v e 服务检测异常 {START_DATE}_hive服务检测异常 STARTDATEhive服务检测异常{DB_ID}&#…...

Gaussian Splatting 在 Ubuntu22.04 下部署
代码:graphdeco-inria/gaussian-splatting (github) 论文:[2308.04079] 3D Gaussian Splatting for Real-Time Radiance Field Rendering (arxiv.org) 1. 禁用自带驱动 Nouveau Ubuntu 自带的显卡驱动,是非Nvida官方版。在后面装cuda的时候,会报驱动不兼容问题。 1.进入…...

ppt中添加页码(幻灯片编号)及问题解决方案
在幻灯片母版中,选择插入 幻灯片编号 右下角显示幻灯片编号 问题一:母版中没有显示编号 原因可能是母版版式中没有设置显示,勾选即可。 问题二:子母版中没有显示幻灯片 将母版中的编号复制到子母版中。 问题三:应用…...

Flutter 初识:对话框和弹出层
Flutter对话框和弹出层小结 对话框AlertDialog属性解析 showDialog属性解析示例 SimpleDialog示例 AboutDialog属性解析示例 Custom Full-Screen Dialog示例 带动画效果的CustomDialog(showGeneralDialog)属性解析示例 自定义Dialog属性解析示例 输入对话…...

启程与远征Ⅳ--人工智能革命尚未发生
人工智能有望彻底改变工作场所。到目前为止,已经有人工智能工具可以取代或增强每一项工作,并使生产力飞速提升。甚至有许多人预测,文案写作等整个行业将在未来几年内被人工智能工具完全取代。但是,如果你抛开炒作,看看…...

Python教程(十五):IO 编程
目录 专栏列表引言基础概念什么是IO? 同步IO vs 异步IO同步IO(Synchronous IO)异步IO(Asynchronous IO) Python中的IO标准IO标准输入和输出 文件IO文件操作的上下文管理器打开文件读取文件操作内存中的数据 高级文件操…...

Qt窗口交互场景、子窗口数据获取
一、前言 在现代软件开发中,图形用户界面(GUI)的设计不仅仅关乎美观,更在于用户体验和功能的无缝衔接。Qt框架以其强大的跨平台能力和丰富的组件库,成为众多开发者构建GUI应用的首选工具。在Qt应用中,窗口…...
【C++学习笔记 18】C++中的隐式构造函数
举个例子 #include <iostream> #include <string>using String std::string;class Entity{ private:String m_Name;int m_Age; public:Entity(const String& name):m_Name(name), m_Age(-1) {}Entity(int age) : m_Name("UnKnown"), m_Age(age) {}…...

单元训练01:LED指示灯的基本控制
蓝桥杯 小蜜蜂 单元训练01:LED指示灯的基本控制 #include "stc15f2k60s2.h" #include <intrins.h>#define LED(x) \{ \P2 P2 & 0x1f | 0x80; \P0 x; \P2 & 0x1f; \}…...

Sanic 和 Go Echo 对比
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storm…...

内部排序(插入、交换、选择)
一、排序的部分基本概念 1. 算法的稳定性 若待排序表中有两个元素 Ri 和 Rj ,其对应的关键字相同即 keyi keyj,且在排序前 Ri 在 Rj 的前面,若使用某一排序算法排序后,Ri 仍然在 Rj 的前面,则称这个排序算法是稳定的…...
Vue3的多种组件通信方式
父组件向子组件传递数据 (Props) 父组件 <template><child :name"name"></child> </template><script setup> import { ref } from vue import Child from ./Child.vueconst name ref(小明) </script> 子组件 <template…...

【C++语言】list的构造函数与迭代器
1. list的介绍及使用 1.1 list的介绍 list的文档介绍 1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。 2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...
逻辑回归:给不确定性划界的分类大师
想象你是一名医生。面对患者的检查报告(肿瘤大小、血液指标),你需要做出一个**决定性判断**:恶性还是良性?这种“非黑即白”的抉择,正是**逻辑回归(Logistic Regression)** 的战场&a…...

什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错
出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果
如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...

python执行测试用例,allure报乱码且未成功生成报告
allure执行测试用例时显示乱码:‘allure’ �����ڲ����ⲿ���Ҳ���ǿ�&am…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...
探索Selenium:自动化测试的神奇钥匙
目录 一、Selenium 是什么1.1 定义与概念1.2 发展历程1.3 功能概述 二、Selenium 工作原理剖析2.1 架构组成2.2 工作流程2.3 通信机制 三、Selenium 的优势3.1 跨浏览器与平台支持3.2 丰富的语言支持3.3 强大的社区支持 四、Selenium 的应用场景4.1 Web 应用自动化测试4.2 数据…...