默认的加锁逻辑是非公平的. 在加锁失败时, 线程会进入 while 循环, 一直尝试获得锁, 这时候是多线程进行竞争. 就是说谁抢到就是谁的.
前言
默认的加锁逻辑是非公平的.
在加锁失败时, 线程会进入 while 循环, 一直尝试获得锁, 这时候是多线程进行竞争. 就是说谁抢到就是谁的.
Redisson 提供了公平锁机制, 使用方式如下:
- RLock fairLock = redisson.getFairLock("anyLock");
- // 最常见的使用方法
- fairLock.lock();
下面一起看下公平锁是如何实现的?
1 公平锁
相信小伙伴们看过前面的文章, 已经轻车熟路了, 直接定位到源码方法: RedissonFairLock#tryLockInnerAsync.
好家伙, 这一大块代码, 我截图也截不完, 咱们直接分析 lua 脚本.
PS: 虽然咱不懂 lua, 但是这一堆堆的 if else 咱们大概还是能看懂的.
因为 debug 发现 command == RedisCommands.EVAL_LONG, 所以直接看下面一部分.
这么长, 连呼好几声好家伙!
先来看看参数都有啥?
KEYS[1]: 加锁的名字, anyLock;
KEYS[2]: 加锁等待队列, redisson_lock_queue:{anyLock};
KEYS[3]: 等待队列中线程锁时间的 set 集合, redisson_lock_timeout:{anyLock}, 是按照锁的时间戳存放到集合中的;
ARGV[1]: 锁超时时间 30000;
ARGV[2]:UUID:ThreadId 组合 a3da2c83-b084-425c-a70f-5d9a08b37f31:1;
ARGV[3]:threadWaitTime 默认 300000;
ARGV[4]:currentTime 当前时间戳.
加锁队列和集合是含有大括号的字符串.{XXXX} 是指这个 key 仅使用 XXXX 用来计算 slot 的位置.
2Lua 脚本分析
上面的 lua 脚本是分为几块的, 咱们分别从不同的角度看下上面代码的执行.
首次加锁(Thread1)
第一部分, 因为是首次加锁, 所以等待队列为空, 直接 跳出循环. 这一部分执行结束.
第二部分:
当锁不存在, 等待队列为空或队首是当前线程, 两个条件都满足时, 进入内部逻辑;
从等待队列和超时集合中删除当前线程, 这时候等待队列和超时集合都是空的, 不需要任何操作;
减少队列中所有等待线程的超时时间, 也不需要任何操作;
加锁并设置超时时间.
执行完这里就 return 了. 所以后面几部分就暂时不看了.
相当于下面两个命令(整个 lua 脚本都是原子的!):
- > hset anyLock a3da2c83-b084-425c-a70f-5d9a08b37f31:1 1
- > pexpire anyLock 30000
Thread2 加锁
当 Thread1 加锁完成之后, 此时 Thread2 来加锁.
Thread2 可以是本实例其他线程, 也可以是其他实例的线程.
第一部分, 虽然锁被 Thread1 占用了, 但是等待队列是空的, 直接跳出循环.
第二部分, 锁存在, 直接跳过.
第三部分, 线程是否持锁, 没有持锁, 直接跳过.
第四部分, 线程是否在等待队列中, Thread2 才来加锁, 不在里面, 直接跳过.
Thread2 最后会来到这里:
从线程等待队列 redisson_lock_queue:{anyLock} 中获取最后一个线程;
因为等待队列是空的, 所以直接获取当前锁的剩余时间 ttl anyLock;
组装超时时间 timeout = ttl + 300000 + 当前时间戳, 这个 300000 是默认 60000*5;
使用 zadd 将 Thread2 放到等待线程有序集合, 然后使用 rpush 将 Thread2 再放到等待队列中.
zadd KEYS[3] timeout ARGV[2]
这里使用 zadd 命令分别放置的是, redisson_lock_timeout:{anyLock}, 超时时间戳(1624612689520), 线程(UUID2:Thread2).
其中超时时间戳当分数, 用来在有序集合中排序, 表示加锁的顺序.
Thread3 加锁
Thread1 占有了锁, Thread2 在等待, 此时线程 3 来了.
获取 firstThreadId2 此时队列是有线程的是 UUID2:Thread2.
判断 firstThreadId2 的分数 (超时时间戳) 是不是小于当前时间戳:
小于等于则说明超时了, 移除 firstThreadId2;
大于, 则会进入后续判断.
第二, 三, 四部分都不满足条件.
Thread3 最后也会来到这里:
从线程等待队列 redisson_lock_queue:{anyLock} 中获取最后一个线程;
最后一个线程存在, 且不是自己, 则 ttl = lastThreadId 超时时间戳 - 当前时间戳, 就是看最后一个线程还有多久超时;
组装超时时间 timeout = ttl + 300000 + 当前时间戳, 这个 300000 是默认 60000*5, 在最后一个线程的超时时间上加上 300000 以及当前时间戳, 就是 Thread3 的超时时间戳.
使用 zadd 将 Thread3 放到等待线程有序集合, 然后使用 rpush 将 Thread3 再放到等待队列中.
3 总结
本文主要总结了公平锁的加锁逻辑, 这里涉及到比较多的 Redis 操作, 做一下简要总结:
Redis Hash 数据结构: 存放当前锁, Redis Key 就是锁, Hash 的 field 是加锁线程, Hash 的 value 是 重入次数;
Redis List 数据结构: 充当线程等待队列, 新的等待线程会使用 rpush 命令放在队列右边;
Redis sorted set 有序集合数据结构: 存放等待线程的顺序, 分数 score 用来是等待线程的超时时间戳.
需要理解的就是这里会额外添加一个等待队列, 以及有序集合.
来源: http://developer.51cto.com/art/202106/669105.htm