持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第24天,点击查看活动详情
公平锁:保证客户端取锁的顺序跟请求获取锁的顺序一致。对应官网:文档
排队:先申请锁,先获取锁。
RedissonFairLock
是 RedissonLock
的子类,整体的锁的技术框架的实现,都是跟 RedissonLock
是一样的,无非就是重载了一些方法,加锁和释放锁的 lua
脚本的逻辑稍微复杂了一些。
对比公平和非公平的优缺点:
优势 | 缺陷 | |
---|---|---|
公平锁 | 各线程公平平等,每个线程在等待一段时间后,总有执行的机会 | 更慢,吞吐量小 |
不公平锁 | 更快,吞吐量更大 | 有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行 |
公平锁与不公平锁的区别在于:获取锁时,队列中是否已有等待的,有则去排队。
分布式锁的公平锁逻辑上更为复杂些。
如果让凡凡来实现公平锁,凡凡能想到什么?
- 获取锁:为了公平,就需要队列:来了就先排队
- 释放锁:通知队列中的线程
举个栗子:
@Test
public void test() {
RLock lock = redisson.getFairLock("fairLock");
lock.lock();
lock.unlock();
}
(2)释放锁
实现加锁的 lua
脚本定位: RedissonFairLock#unlockInnerAsync
对应参数如下:
-
KEYS[1]
:锁的名称 "fairLock
" -
KEYS[2]
: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]
:redisson_lock_timeout:{fairLock}
, 基于Redis
实现的Set
集合,作为超时集合有序集合,可以自动按照给每个数据指定的分数
score
来进行排序。 -
ARGV[1]
:LockPubSub.UNLOCK_MESSAGE
,要发送的 -
ARGV[2]
:30000毫秒,看门狗超时时间 -
ARGV[3]
:线key
名称,UUID:threadId
-
ARGV[4]
:时间戳,当前时间(10:00:00)的时间戳
-- #1
-- 无限循环:
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;
-- #2
-- 锁不存在
if (redis.call('exists', KEYS[1]) == 0) then
-- Redis的消息订阅去通知,队列首元素(线程)来获取锁
local nextThreadId = redis.call('lindex', KEYS[2], 0);
if nextThreadId ~= false then
redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]);
end;
return 1;
end;
-- #3
-- 锁存在但不是持有锁的不是当前线程,则退出,返回 null
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
-- #4 .1
-- 重入锁 -1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-- #4 .2
-- 如果当前线程还有地方持有锁,则续命
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
end;
-- #5
-- 删除锁
redis.call('del', KEYS[1]);
-- 获取队列当前第一个元素(线程)
local nextThreadId = redis.call('lindex', KEYS[2], 0);
if nextThreadId ~= false then
-- Redis的消息订阅,去通知
redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]);
end;
return 1;
释放锁相对于加锁容易些,大致流程如下:
- 清理队列中过期线程:无限循环
- 锁不存在:通知队首线程来获取锁
- 锁存在但持有锁的不是当前线程:直接退出,返回
null
- 获取重入锁数量并 -1,若
counter > 0
则继续续命 - 删除锁,并通知队首线程来获取锁
总结,公共平锁的释放可分为 主动释放 和 超时释放:
-
主动释放:即调用
unlock()
方法 -
超时释放:设定超时时间,过时就释放了。
- 宕机了,看门狗就不续命,过段时间也就释放锁了。