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的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点…...
day52 ResNet18 CBAM
在深度学习的旅程中,我们不断探索如何提升模型的性能。今天,我将分享我在 ResNet18 模型中插入 CBAM(Convolutional Block Attention Module)模块,并采用分阶段微调策略的实践过程。通过这个过程,我不仅提升…...
2025盘古石杯决赛【手机取证】
前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来,实在找不到,希望有大佬教一下我。 还有就会议时间,我感觉不是图片时间,因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...
UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)
UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中,UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
第 86 场周赛:矩阵中的幻方、钥匙和房间、将数组拆分成斐波那契序列、猜猜这个单词
Q1、[中等] 矩阵中的幻方 1、题目描述 3 x 3 的幻方是一个填充有 从 1 到 9 的不同数字的 3 x 3 矩阵,其中每行,每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的row x col 的 grid,其中有多少个 3 3 的 “幻方” 子矩阵&am…...
iOS性能调优实战:借助克魔(KeyMob)与常用工具深度洞察App瓶颈
在日常iOS开发过程中,性能问题往往是最令人头疼的一类Bug。尤其是在App上线前的压测阶段或是处理用户反馈的高发期,开发者往往需要面对卡顿、崩溃、能耗异常、日志混乱等一系列问题。这些问题表面上看似偶发,但背后往往隐藏着系统资源调度不当…...
MySQL 部分重点知识篇
一、数据库对象 1. 主键 定义 :主键是用于唯一标识表中每一行记录的字段或字段组合。它具有唯一性和非空性特点。 作用 :确保数据的完整性,便于数据的查询和管理。 示例 :在学生信息表中,学号可以作为主键ÿ…...
R 语言科研绘图第 55 期 --- 网络图-聚类
在发表科研论文的过程中,科研绘图是必不可少的,一张好看的图形会是文章很大的加分项。 为了便于使用,本系列文章介绍的所有绘图都已收录到了 sciRplot 项目中,获取方式: R 语言科研绘图模板 --- sciRplothttps://mp.…...
提升移动端网页调试效率:WebDebugX 与常见工具组合实践
在日常移动端开发中,网页调试始终是一个高频但又极具挑战的环节。尤其在面对 iOS 与 Android 的混合技术栈、各种设备差异化行为时,开发者迫切需要一套高效、可靠且跨平台的调试方案。过去,我们或多或少使用过 Chrome DevTools、Remote Debug…...
