【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现

引言

在本篇中,我们继续探索redisson中相关锁的实现,本期将围绕公平锁进行讲解。在正式开始前,我们回顾下公平锁的概念-在多线程或分布式环境中,锁的获取是有先后顺序的,按照请求顺序来获得锁。这就意味着A、B、C 三个线程都想获得同一把锁,那么最先请求的线程会被优先给予资源。如果此时锁被某个线程占用,其余线程会根据各自的排队顺序来抢占锁,谁排在前面,谁就先获得锁。

在 Redisson 中,对分布式 Redis 锁的公平性,就是说锁的获取需要按照先来后到排队,避免后来的请求"插队"。

加锁

我们先来看看如何实现加锁的,在 RedissonFairLock 类里,我们可以找到核心加锁逻辑:

java 复制代码
public void lock() {
    try {
        // 1. 尝试获取锁
        if (tryAcquire()) {
            return;
        }
        
        // 2. 获取失败,将当前线程信息写入等待队列
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquisition(threadId);
        
        // 3. 循环等待获取锁
        while (ttl != null) {
            // 订阅锁释放消息
            subscribe(threadId);
            // 等待锁释放通知
            await(ttl);
            // 重试获取锁
            ttl = tryAcquisition(threadId);
        }
    } finally {
        // 取消订阅
        unsubscribe(Thread.currentThread().getId());
    }
}

可以看到调用tryAcquisition方法来尝试获取锁,这个方法里则封装了相关的Lua脚本,如下:

java 复制代码
private Long tryAcquisition(long threadId) {
    return redis.eval("""
        -- 检查锁是否已被占用
        if (redis.call('exists', KEYS[1]) == 0) then
            -- 获取锁并设置threadId
            redis.call('hset', KEYS[1], ARGV[2], 1);
            redis.call('pexpire', KEYS[1], ARGV[1]);
            -- 将线程加入等待队列末尾
            redis.call('zadd', KEYS[2], ARGV[3], ARGV[2]);
            return nil;
        end;
        
        -- 检查是否是队列第一个等待者
        local firstThreadId = redis.call('zrange', KEYS[2], 0, 0);
        if (firstThreadId[1] == ARGV[2]) then
            -- 获取锁
            redis.call('hset', KEYS[1], ARGV[2], 1);
            redis.call('pexpire', KEYS[1], ARGV[1]);
            -- 从等待队列移除
            redis.call('zrem', KEYS[2], ARGV[2]);
            return nil;
        end;
        
        -- 返回需要等待的时间
        return redis.call('pttl', KEYS[1]);
    """);

在尝试获取锁时:首先尝试直接获取锁;如果获取失败,将线程信息加入等待队列;只有当前线程是等待队列的第一个时才能获取锁;获取失败则订阅锁释放消息并等待;等待后重试获取锁,直到成功。

释放锁

看完了加锁,我们继续看释放锁,释放锁的核心逻辑在于Lua脚本的实现:

java 复制代码
public void unlock() {
    redis.eval("""
        -- 检查是否是锁持有者
        if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then
            return nil;
        end;
        
        -- 减少重入计数
        local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
        if (counter > 0) then
            redis.call('pexpire', KEYS[1], ARGV[1]);
            return 0;
        end;
        
        -- 删除锁
        redis.call('del', KEYS[1]);
        -- 发布释放消息
        redis.call('publish', KEYS[2], ARGV[2]);
        return 1;
    """);

解锁过程:验证当前线程是否是锁的持有者;处理重入计数的递减;当计数为0时完全释放锁;发布锁释放消息通知等待线程。

为何能保障"公平"?

1.先排队

Redisson 在请求 lock 时,会先把线程标识以一定的 score(通常是时间戳)存储进 ZSet。这样就等于"先到先排队"。

2.只允许队首"抢"锁

脚本会检查 ZSet 的首位元素(ZRANGE KEYS[2], 0, 0),只有首位(rank == 0)的线程才有真正"尝试加锁"的资格。

3.不可插队

因为 Lua 脚本在 Redis 里是原子执行的,不会出现其他线程"并发修改队列"的情况,且 ZSet 的 score 是根据时间先后确定顺序。不会让后来的线程"插队"。

4.线程之间实时通知

当锁释放后,publish 通知所有订阅方,后面排队的线程就能及时知道"锁可以重新争抢了",然后再调用加锁脚本进行排名检查,最终实现"先来先拿"。

小结

通过这些 Lua 脚本的实现逻辑,相信各位读者已经理解了 Redisson 公平锁为什么能做到"先来先得",以及其在分布式环境下是如何保证线程安全、互不干扰的。还是那句话,看源码的目的在于学习别人优秀的设计,希望对大家有所启发!

相关推荐
盛世宏博智慧档案19 分钟前
全国100个分布式机房环境温湿度智能监测系统建设方案
分布式·机房·温湿度
东风破_22 分钟前
TypeScript 高级类型进阶:keyof、Exclude、Record 与类型组合思想
前端·后端·typescript
凤山老林42 分钟前
数据库读写分离与动态路由实战:Spring Boot + ShardingSphere-JDBC 生产配置
数据库·spring boot·后端·分库分表·sharding-jdbc
六bring个六1 小时前
分布式设备连接对端失败问题分析
分布式·分布式软总线
Zane19941 小时前
调用了 async 函数却没执行?一文讲透协程、event loop 与 await
后端·python
Zane19941 小时前
HashMap 为什么要在长度16、容量必须是2的幂这些细节上较劲
java·后端
掘金者阿豪2 小时前
若依启动突然报 Redis MISCONF?一次从应用报错到磁盘爆满的完整排查记录
后端
boooooooom3 小时前
尝尝咸淡:从朴素 RAG 到 Graph RAG——一个烹饪问答系统的三层检索升级之路
前端·后端·llm
福大大架构师每日一题3 小时前
ragflow v0.27.0 发布:Go 后端全面对齐、智能体搜索与知识编译大升级,超全更新解析
开发语言·后端·golang
2601_963870184 小时前
【计算机毕业设计】基于Vue+Spring Boot的助农电商系统的设计与实现
spring boot·后端·课程设计