当前位置: 首页 > news >正文

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]);

http://www.lryc.cn/news/425352.html

相关文章:

  • 冷热数据拆分
  • JavaScript 基础(四)
  • 《机器学习by周志华》学习笔记-神经网络-01神经元模型
  • C#中常用的扩展类
  • 麒麟v10(ky10.x86_64)升级——openssl-3.2.2、openssh-9.8p1
  • 【Unity】有限状态机和抽象类多态
  • KETTLE调用http传输中文参数的问题
  • Gaussian Splatting 在 Ubuntu22.04 下部署
  • ppt中添加页码(幻灯片编号)及问题解决方案
  • Flutter 初识:对话框和弹出层
  • 启程与远征Ⅳ--人工智能革命尚未发生
  • Python教程(十五):IO 编程
  • Qt窗口交互场景、子窗口数据获取
  • 【C++学习笔记 18】C++中的隐式构造函数
  • 单元训练01:LED指示灯的基本控制
  • Sanic 和 Go Echo 对比
  • 内部排序(插入、交换、选择)
  • Vue3的多种组件通信方式
  • 【C++语言】list的构造函数与迭代器
  • Python 安装 PyTorch详细教程
  • html页面缩放自适应
  • 024.自定义chormium-修改屏幕尺寸
  • 测试环境搭建整套大数据系统(十九:kafka3.6.0单节点做 sasl+acl)
  • 小白零基础学数学建模应用系列(五):任务分配问题优化与求解
  • 怎么防止源代码泄露?十种方法杜绝源代码泄密风险
  • uniapp left right 的左右模态框
  • Docker Compose与私有仓库部署
  • Layout 布局组件快速搭建
  • 北京城市图书馆-非遗文献馆:OLED透明拼接屏的璀璨应用
  • OpenCV图像滤波(12)图像金字塔处理函数pyrDown()的使用