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

简介: 本文深入解析了 Redisson 中公平锁的实现原理。公平锁通过确保线程按请求顺序获取锁,避免“插队”现象。在 Redisson 中,`RedissonFairLock` 类的核心逻辑包含加锁与解锁两部分:加锁时,线程先尝试直接获取锁,失败则将自身信息加入 ZSet 等待队列,只有队首线程才能获取锁;解锁时,验证持有者身份并减少重入计数,最终删除锁或通知等待线程。其“公平性”源于 Lua 脚本的原子性操作:线程按时间戳排队、仅队首可尝试加锁、实时发布锁释放通知。这些设计确保了分布式环境下的线程安全与有序执行。

引言

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

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

加锁

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

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脚本,如下:

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脚本的实现:

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 公平锁为什么能做到“先来先得”,以及其在分布式环境下是如何保证线程安全、互不干扰的。还是那句话,看源码的目的在于学习别人优秀的设计,希望对大家有所启发!

目录
相关文章
|
8天前
|
NoSQL 调度 Redis
分布式锁—3.Redisson的公平锁
Redisson公平锁(RedissonFairLock)是一种基于Redis实现的分布式锁,确保多个线程按申请顺序获取锁,从而实现公平性。其核心机制是通过队列和有序集合管理线程的排队顺序。加锁时,线程会进入队列并等待,锁释放后,队列中的第一个线程优先获取锁。RedissonFairLock支持可重入加锁,即同一线程多次加锁不会阻塞。新旧版本在排队机制上有所不同,新版本在5分钟后才会重排队列,而旧版本在5秒后就会重排。释放锁时,Redisson会移除队列中等待超时的线程,并通知下一个排队的线程获取锁。通过这种机制,RedissonFairLock确保了锁的公平性和顺序性。
|
6天前
|
NoSQL 调度 Redis
分布式锁—5.Redisson的读写锁
Redisson读写锁(RedissonReadWriteLock)是Redisson提供的一种分布式锁机制,支持读锁和写锁的互斥与并发控制。读锁允许多个线程同时获取,适用于读多写少的场景,而写锁则是独占锁,确保写操作的互斥性。Redisson通过Lua脚本实现锁的获取、释放和重入逻辑,并利用WatchDog机制自动续期锁的过期时间,防止锁因超时被误释放。 读锁的获取逻辑通过Lua脚本实现,支持读读不互斥,即多个线程可以同时获取读锁。写锁的获取逻辑则确保写写互斥和读写互斥,即同一时间只能有一个线程获取写锁,
|
6天前
|
算法 NoSQL Redis
分布式锁—4.Redisson的联锁和红锁
Redisson的MultiLock和RedLock机制为分布式锁提供了强大的支持。MultiLock允许一次性锁定多个资源,确保在更新这些资源时不会被其他线程干扰。它通过将多个锁合并为一个大锁,统一进行加锁和释放操作。RedissonMultiLock的实现通过遍历所有锁并尝试加锁,若在超时时间内无法获取所有锁,则释放已获取的锁并重试。 RedLock算法则基于多个Redis节点的加锁机制,确保在大多数节点上加锁成功即可。RedissonRedLock通过重载MultiLock的failedLocksLi
|
6天前
|
NoSQL Java Redis
分布式锁—6.Redisson的同步器组件
Redisson提供了多种分布式同步工具,包括分布式锁、Semaphore和CountDownLatch。分布式锁包括可重入锁、公平锁、联锁、红锁和读写锁,适用于不同的并发控制场景。Semaphore允许多个线程同时获取锁,适用于资源池管理。CountDownLatch则用于线程间的同步,确保一组线程完成操作后再继续执行。Redisson通过Redis实现这些同步机制,提供了高可用性和高性能的分布式同步解决方案。源码剖析部分详细介绍了这些组件的初始化和操作流程,展示了Redisson如何利用Redis命令和
|
2月前
|
数据采集 存储 数据可视化
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
211 0
分布式爬虫框架Scrapy-Redis实战指南
|
3月前
|
NoSQL Java 中间件
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
本文介绍了从单机锁到分布式锁的演变,重点探讨了使用Redis实现分布式锁的方法。分布式锁用于控制分布式系统中多个实例对共享资源的同步访问,需满足互斥性、可重入性、锁超时防死锁和锁释放正确防误删等特性。文章通过具体示例展示了如何利用Redis的`setnx`命令实现加锁,并分析了简化版分布式锁存在的问题,如锁超时和误删。为了解决这些问题,文中提出了设置锁过期时间和在解锁前验证持有锁的线程身份的优化方案。最后指出,尽管当前设计已解决部分问题,但仍存在进一步优化的空间,将在后续章节继续探讨。
614 131
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
|
3天前
|
缓存 监控 NoSQL
Redis设计与实现——分布式Redis
Redis Sentinel 和 Cluster 是 Redis 高可用与分布式架构的核心组件。Sentinel 提供主从故障检测与自动切换,通过主观/客观下线判断及 Raft 算法选举领导者完成故障转移,但存在数据一致性和复杂度问题。Cluster 支持数据分片和水平扩展,基于哈希槽分配数据,具备自动故障转移和节点发现机制,适合大规模高并发场景。复制机制包括全量同步和部分同步,通过复制积压缓冲区优化同步效率,但仍面临延迟和资源消耗挑战。两者各有优劣,需根据业务需求选择合适方案。
|
4天前
|
数据采集 存储 NoSQL
基于Scrapy-Redis的分布式景点数据爬取与热力图生成
基于Scrapy-Redis的分布式景点数据爬取与热力图生成
|
13天前
|
数据采集 存储 NoSQL
分布式爬虫去重:Python + Redis实现高效URL去重
分布式爬虫去重:Python + Redis实现高效URL去重
|
3月前
|
NoSQL Java Redis
Springboot使用Redis实现分布式锁
通过这些步骤和示例,您可以系统地了解如何在Spring Boot中使用Redis实现分布式锁,并在实际项目中应用。希望这些内容对您的学习和工作有所帮助。
245 83

热门文章

最新文章