持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情
公平锁:保证客户端取锁的顺序跟请求获取锁的顺序一致。对应官网:文档
排队:先申请锁,先获取锁。
RedissonFairLock
是 RedissonLock
的子类,整体的锁的技术框架的实现,都是跟 RedissonLock
是一样的,无非就是重载了一些方法,加锁和释放锁的 lua
脚本的逻辑稍微复杂了一些。
对比公平和非公平的优缺点:
优势 | 缺陷 | |
---|---|---|
公平锁 | 各线程公平平等,每个线程在等待一段时间后,总有执行的机会 | 更慢,吞吐量小 |
不公平锁 | 更快,吞吐量更大 | 有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行 |
公平锁与不公平锁的区别在于:获取锁时,队列中是否已有等待的,有则去排队。
分布式锁的公平锁逻辑上更为复杂些。
如果让凡凡来实现公平锁,凡凡能想到什么?
- 获取锁:为了公平,就需要队列:来了就先排队
- 释放锁:通知队列中的线程
举个栗子:
@Test
public void test() {
RLock lock = redisson.getFairLock("fairLock");
lock.lock();
lock.unlock();
}
(1)加锁
实现加锁的 lua
脚本定位: RedissonFairLock#tryLockInnerAsync
对应参数如下:
-
KEYS[1]
:锁的名称 "fairLock
" -
KEYS[2]
:threadsQueueName = redisson_lock_queue:{fairLock}
, 基于Redis
实现的队列,作为等待队列lindex redisson_lock_queue:{fairLock} 0
:从redisson_lock_queue:{fairLock}
这个队列中弹出来第一个元素lpop redisson_lock_queue:{fairLock}
:弹出队列的第一个元素zrem redisson_lock_timeout:{fairLock} UUID:threadId
:从Set
集合中删除threadId
对应的元素
-
KEYS[3]
:timeoutSetName = redisson_lock_timeout:{fairLock}
, 基于Redis
实现的Set
集合,作为超时集合有序集合,可以自动按照给每个数据指定的分数
score
来进行排序。 -
ARGV[1]
:30000毫秒 -
ARGV[2]
:key
名称,UUID:threadId
-
ARGV[3]
:线程等待时间,默认 5000毫秒 -
ARGV[4]
:时间戳,当前时间(10:00:00)的时间戳
-- #1
-- 无限循环:移除过时无用的线程
while true do
-- 从队列中弹出第一个值
local firstThreadId2 = redis.call('lindex', KEYS[2], 0);
-- 如果队列为空,则退出循环
if firstThreadId2 == false then
break;
end;
-- 移除超时的线程
-- 从 set 集合(redisson_lock_timeout:{fairLock}) 中获取第一个元素的分数,即时间戳
local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));
if timeout <= tonumber(ARGV[3]) then -- 如果小于了超时时间了
redis.call('zrem', KEYS[3], firstThreadId2); -- 从 set 中移除
redis.call('lpop', KEYS[2]); -- 从 队列 中移除
else
break;
end;
end;
-- #2 .1
-- 锁不存在 且 (队列不存在 或者 队列存在,但是队头第一个元素是当前这个线程 UUID:threadId)
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]); -- 从 set 集合中删除 threadId对应的元素
-- 减少队列中所有等待线程的超时时间
local keys = redis.call('zrange', KEYS[3], 0, -1); -- 获取所有的 keys:线程
for i = 1, #keys, 1 do
redis.call('zincrby', KEYS[3], -tonumber(ARGV[4]), keys[i]);
end;
-- 当前线程获得锁
-- 存储结构:fairLock: {"UUID:threadId": 1}
redis.call('hset', KEYS[1], ARGV[2], 1); -- 加锁
redis.call('pexpire', KEYS[1], ARGV[1]); -- 将这锁的生存时间设置为 30000 毫秒
return nil; -- 认为加锁成功,并会开启一个 watchdog 看门狗定时调度
end;
-- #2 .2
-- 这里是可重入的代码
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;
-- #2 .3
-- 说明当前线程在队列中
local timeout = redis.call('zscore', KEYS[3], ARGV[2]); -- 获取当前线程的超时时间戳
if timeout ~= false then -- 当前线程超时时间不为空
-- 计算当前线程,还需要等待多久
-- 需要等待时间 = 超时时间 - 线程需要等待的时间 - 线程
return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);
end;
-- #3
-- 取出队列中最后一个线程
local lastThreadId = redis.call('lindex', KEYS[2], -1);
local ttl;
-- 最后线程不等于当前线程
if lastThreadId ~= false and lastThreadId ~= ARGV[2] then
-- 剩余生存时间 = 最后线程时间戳 - 当前时间的时间戳
ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);
else
-- 获取当前锁 key 的生存时间
ttl = redis.call('pttl', KEYS[1]);
end;
-- #4
-- 计算,再入队
local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);
if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then
redis.call('rpush', KEYS[2], ARGV[2]);
end;
-- 这块如何理解:
-- 1. 如果当前线程是持有锁的,则返回锁的生存时间
-- 2. 如果当前线程是不持有锁,则返回需要等待的时间,即下次再来执行此脚本的时间
return ttl;
概括下大致流程:
-
清理队列中过期线程:无限循环
-
锁是否存在:
- 锁不存在且(队列为空或者队首线程就是当前线程):获得锁
- 锁存在且持有锁的线程是当前线程:重入锁,重入值 +1
- 锁存在,持有锁的线程不是当前线程且在队列中:直接返回还要等待多久
-
计算生存时间
-
新来的或者过期被清理的:重新入队,延长等待时间
这里有个问题:内鬼问题、信任问题
- 锁处理严重依赖时间戳:倘若某台机器时钟出现问题
- 内鬼问题:如果不同版本实现的锁处理不同,这块是不是就会出现问题。
举个栗子:3个客户端顺序加锁
- 客户端A:
UUID_00:threadId_00
,10:00:00
去请求锁 - 客户端B:
UUID_01:threadId_01
,10:00:10
去请求锁 - 客户端C:
UUID_02:threadId_02
,与客户端B同时去请求锁,差 5毫秒
- 客户端A在
10:00:00
请求获取锁fairLock
(这时还未有锁)
-
大概 10秒后
10:00:10
,客户端B也去请求获取锁ttl
= 锁生存时间,20000毫秒timeout
=ttl
+10:00:10
+ 5000毫秒(默认)=10:00:35
-
同时客户端C也去请求获取锁,差 5毫秒
ttl
=10:00:35
(队列最后一个线程的时间,即客户端B)-10:00:15
(当前时间) = 20000毫秒timeout
= 20000毫秒 +10:00:15
+ 5000毫秒 =10:00:40