【Redisson】公平锁源码剖析 之 释放锁

176 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第24天,点击查看活动详情

公平锁:保证客户端取锁的顺序跟请求获取锁的顺序一致。对应官网:文档

排队:先申请锁,先获取锁。

RedissonFairLockRedissonLock 的子类,整体的锁的技术框架的实现,都是跟 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;

释放锁相对于加锁容易些,大致流程如下:

  1. 清理队列中过期线程:无限循环
  2. 锁不存在:通知队首线程来获取锁
  3. 锁存在但持有锁的不是当前线程:直接退出,返回 null
  4. 获取重入锁数量并 -1,若 counter > 0 则继续续命
  5. 删除锁,并通知队首线程来获取锁

总结,公共平锁的释放可分为 主动释放 和 超时释放:

  • 主动释放:即调用 unlock() 方法

  • 超时释放:设定超时时间,过时就释放了。

    • 宕机了,看门狗就不续命,过段时间也就释放锁了。