【📕分布式锁通关指南 11】源码剖析redisson之读写锁的实现

简介: Redisson 的 `RedissonReadWriteLock` 提供了高效的分布式读写锁实现,适用于读多写少的场景。通过 Redis 与 Lua 脚本结合,确保读锁并行、写锁互斥,以及读写之间的互斥,保障了分布式环境下的数据一致性。它支持可重入、自动过期和锁释放机制,提升了系统并发性能与资源控制能力。

引言

本期我们将深入了解 Redisson 提供的另一个常用分布式锁形态——读写锁(ReadWriteLock) 。在实际业务场景中,对于同一关键资源,不同线程可能只需要“读取”或者“写入”,如果能让读操作并行、而写操作互斥,就能大大提升效率。Redisson 为我们提供了一个典型的读写锁实现,让分布式读多写少的场景更具伸缩性。

介绍

在分布式的场景下,有些数据是“读远多于写”的。比如一些基础配置、商品信息等,这类场景往往更适合使用读写锁——对于读操作只要不涉及数据修改,就可以同时进行;一旦需要写操作,就要独占保持一致性。Redisson 在此基础之上封装了分布式的读写锁 API,使得我们可以把单机环境下的读写锁模式,透明地延伸到集群。

它的常见应用包括:

  • 分布式缓存的配置数据:读操作要并发读,而更新操作需要阻塞所有读操作。
  • 某些共享资源的高并发访问场景:比如日志、统计信息等,与写操作互斥而与读操作并行。

接下来让我们一窥源码,看看 RedissonReadWriteLock(以下简称 RWWLock)是如何实现其读写互斥的。

读写锁的实现思路

和 JDK 自带的 java.util.concurrent.locks.ReadWriteLock 类似,Redisson 的读写锁同样提供两种锁:

  1. 读锁(RLock 类型)
  2. 写锁(RLock 类型)

只不过它会分别负责“读 + 读不互斥、写 + 读写互斥”等同步语义,并在底层与 Redis 交互以保证分布式环境的一致性、正确性。

在 Redisson 中,对外暴露的是 RReadWriteLock 接口,后续通过类似来获取读锁或写锁:

RReadWriteLock rwLock = redissonClient.getReadWriteLock("anyRWLock");
RLock readLock = rwLock.readLock();
RLock writeLock = rwLock.writeLock();

核心结构

RedissonReadWriteLock 主要包含以下核心逻辑:

  • 通过 Redis 的 Lua 脚本来保证读锁的可重入及与写锁的互斥关系。
  • 每获取一次读锁,会在 Redis 维护一个计数,用于标识该读锁被多少线程持有。
  • 获取写锁时,需要判断 Redis 中是否已有读锁或写锁被占用,若被占用则需要阻塞或返回获取失败。

源码剖析

看看 RedissonReadWriteLock 的部分源码(为了说明简化展示):

public class RedissonReadWriteLock implements RReadWriteLock {
   

    final CommandAsyncExecutor commandExecutor;
    private final String writeLockName;
    private final String readLockName;

    public RedissonReadWriteLock(CommandAsyncExecutor commandExecutor, String name) {
   
        this.commandExecutor = commandExecutor;
        // 为读写锁分别构造不同的 key
        this.writeLockName = "redisson_rwlock{" + name + "}:write";
        this.readLockName = "redisson_rwlock{" + name + "}:read";
    }

    @Override
    public RLock readLock() {
   
        return new RedissonReadLock(commandExecutor, readLockName, writeLockName);
    }

    @Override
    public RLock writeLock() {
   
        return new RedissonWriteLock(commandExecutor, writeLockName, readLockName);
    }
}

可以看到 RedissonReadWriteLock 会根据同一个业务名 name,分别生成写锁 key 和读锁 key。随后,它会分别返回 RedissonReadLockRedissonWriteLock 来完成具体的分布式加解锁逻辑。接下来,让我们聚焦更细节的 RedissonReadLockRedissonWriteLock 实现。

读锁

RedissonReadLock 内部通过 Lua 脚本来实现加锁等操作。简化版的加锁逻辑如下:

public class RedissonReadLock extends RedissonBaseLock {
   

    public RedissonReadLock(CommandAsyncExecutor commandExecutor, String lockName, String writeLockName) {
   
        super(commandExecutor, lockName, LockType.READ);
        this.writeLockName = writeLockName;
    }

    @Override
    public void lock() {
   
        try {
   
            tryLock(-1, null);
        } catch (InterruptedException e) {
   
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
   
        // 关键入口
        return tryAcquire(time, unit);
    }

    private boolean tryAcquire(long waitTime, TimeUnit unit) throws InterruptedException {
   
        // 省略部分细节
        // 核心:通过在Redis执行lua脚本,判断写锁是否被占用、读锁计数等
        // 如果允许加读锁,则对读锁计数加1;否则阻塞或返回false
        return get(tryLockAsync(waitTime, unit));
    }

    // ...
}

tryAcquire 中,会检查当前是否有写锁占用(包括线程自身是否持有写锁),如果没有,则直接对读锁计数 +1 并返回成功;否则需要阻塞或失败退出。

写锁

RedissonWriteLock 的设计也类似,只是检查的逻辑换成——若读锁计数不为 0 或已有其他写锁线程在占用,则无法获取写锁。这些操作同样是通过发送 Lua 脚本到 Redis 来实现原子性的。

public class RedissonWriteLock extends RedissonBaseLock {
   

    public RedissonWriteLock(CommandAsyncExecutor commandExecutor, String lockName, String readLockName) {
   
        super(commandExecutor, lockName, LockType.WRITE);
        this.readLockName = readLockName;
    }

    @Override
    public void lock() {
   
        try {
   
            tryLock(-1, null);
        } catch (InterruptedException e) {
   
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
   
        return tryAcquire(time, unit);
    }

    private boolean tryAcquire(long waitTime, TimeUnit unit) throws InterruptedException {
   
        // 同样省略部分细节
        // 通过Lua脚本判断是否存在读锁计数或被其他写锁占用
        // 如果可加锁,则把写锁 key(val)设为当前线程标识并加1计数
        return get(tryLockAsync(waitTime, unit));
    }

    // ...
}

释放锁

无论是读锁还是写锁,都会在 unlock() 时进行一个 Lua 脚本调用,对计数器进行 -1。如果读锁计数降到 0,说明没人再持有读锁,可以清除对应的 key 同时释放锁资源;写锁同理——当计数回到 0 时,说明可以彻底释放。

如何保证分布式读写锁的一致性?

  1. Lua 脚本原子性
    Redisson 将所有加锁、解锁、自增或自减计数等动作打包到 Lua 脚本中,并在 Redis 服务器端原子执行,避免了“网络传输过程中分步执行可能导致的并发问题”。
  2. 分隔的 key 空间
    读锁与写锁在 Redis 中对应不同的 key,避免互相冲突同时也便于统计读锁计数。但在加锁的 Lua 脚本中,会先检查写锁 key、再检查读锁 key,或反之,以此维持正确关系。
  3. 可重入语义
    当同一线程再次获取读锁或写锁时,Redis 中的计数会递增,继续维持对资源的持有,这对于分布式场景下的可重入需求非常重要。
  4. 自动过期
    和其他 Redisson 锁类似,读写锁也提供 leaseTime 等机制,让锁可以在指定时间后自动释放,防止意外死锁。

小结

RedissonReadWriteLock 在分布式环境下完美复刻了本地读写锁的行为:读锁可并行、写锁需互斥、读写也互斥。通过 Lua 脚本与 Redis 的结合,它保证了加解锁过程的原子性与正确性,同时在可重入、自动续期等方面做了周全考虑。对于多读少写的分布式业务场景,读写锁可以在并发性能与资源互斥之间取得良好平衡,极大地提升系统吞吐量。

阅读源码能让我们更好地理解这套机制背后的原理与设计,也能帮助我们在日常工作中更加灵活地选型和排查问题。希望本文能给大家带来收获,我们下一期再见!

目录
相关文章
|
7月前
|
NoSQL 调度 Redis
分布式锁—3.Redisson的公平锁
Redisson公平锁(RedissonFairLock)是一种基于Redis实现的分布式锁,确保多个线程按申请顺序获取锁,从而实现公平性。其核心机制是通过队列和有序集合管理线程的排队顺序。加锁时,线程会进入队列并等待,锁释放后,队列中的第一个线程优先获取锁。RedissonFairLock支持可重入加锁,即同一线程多次加锁不会阻塞。新旧版本在排队机制上有所不同,新版本在5分钟后才会重排队列,而旧版本在5秒后就会重排。释放锁时,Redisson会移除队列中等待超时的线程,并通知下一个排队的线程获取锁。通过这种机制,RedissonFairLock确保了锁的公平性和顺序性。
|
3月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
226 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
4月前
|
NoSQL Redis
分布式锁设计吗,你是如何实现锁类型切换、锁策略切换基于限流的?
本方案基于自定义注解与AOP实现分布式锁,支持锁类型(如可重入锁、公平锁等)与加锁策略(如重试、抛异常等)的灵活切换,并结合Redisson实现可重入、自动续期等功能,通过LUA脚本保障原子性,兼顾扩展性与实用性。
96 0
|
7月前
|
NoSQL 调度 Redis
分布式锁—5.Redisson的读写锁
Redisson读写锁(RedissonReadWriteLock)是Redisson提供的一种分布式锁机制,支持读锁和写锁的互斥与并发控制。读锁允许多个线程同时获取,适用于读多写少的场景,而写锁则是独占锁,确保写操作的互斥性。Redisson通过Lua脚本实现锁的获取、释放和重入逻辑,并利用WatchDog机制自动续期锁的过期时间,防止锁因超时被误释放。 读锁的获取逻辑通过Lua脚本实现,支持读读不互斥,即多个线程可以同时获取读锁。写锁的获取逻辑则确保写写互斥和读写互斥,即同一时间只能有一个线程获取写锁,
382 17
|
9月前
|
安全
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
Redisson 的看门狗机制是解决分布式锁续期问题的核心功能。当通过 `lock()` 方法加锁且未指定租约时间时,默认启用 30 秒的看门狗超时时间。其原理是在获取锁后创建一个定时任务,每隔 1/3 超时时间(默认 10 秒)通过 Lua 脚本检查锁状态并延长过期时间。续期操作异步执行,确保业务线程不被阻塞,同时仅当前持有锁的线程可成功续期。锁释放时自动清理看门狗任务,避免资源浪费。学习源码后需注意:避免使用带超时参数的加锁方法、控制业务执行时间、及时释放锁以优化性能。相比手动循环续期,Redisson 的定时任务方式更高效且安全。
548 24
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
|
7月前
|
监控 NoSQL Java
分布式锁—2.Redisson的可重入锁
本文主要介绍了Redisson可重入锁RedissonLock概述、可重入锁源码之创建RedissonClient实例、可重入锁源码之lua脚本加锁逻辑、可重入锁源码之WatchDog维持加锁逻辑、可重入锁源码之可重入加锁逻辑、可重入锁源码之锁的互斥阻塞逻辑、可重入锁源码之释放锁逻辑、可重入锁源码之获取锁超时与锁超时自动释放逻辑、可重入锁源码总结。
|
9月前
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
本文深入剖析了Redisson中可重入锁的释放锁Lua脚本实现及其获取锁的两种方式(阻塞与非阻塞)。释放锁流程包括前置检查、重入计数处理、锁删除及消息发布等步骤。非阻塞获取锁(tryLock)通过有限时间等待返回布尔值,适合需快速反馈的场景;阻塞获取锁(lock)则无限等待直至成功,适用于必须获取锁的场景。两者在等待策略、返回值和中断处理上存在显著差异。本文为理解分布式锁实现提供了详实参考。
319 11
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
|
8月前
|
存储 安全 NoSQL
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
本文深入解析了 Redisson 中公平锁的实现原理。公平锁通过确保线程按请求顺序获取锁,避免“插队”现象。在 Redisson 中,`RedissonFairLock` 类的核心逻辑包含加锁与解锁两部分:加锁时,线程先尝试直接获取锁,失败则将自身信息加入 ZSet 等待队列,只有队首线程才能获取锁;解锁时,验证持有者身份并减少重入计数,最终删除锁或通知等待线程。其“公平性”源于 Lua 脚本的原子性操作:线程按时间戳排队、仅队首可尝试加锁、实时发布锁释放通知。这些设计确保了分布式环境下的线程安全与有序执行。
250 0
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
|
11月前
|
SQL Java 关系型数据库
【📕分布式锁通关指南 01】从解决库存超卖开始加锁的初体验
本文通过电商场景中的库存超卖问题,深入探讨了JVM锁、MySQL悲观锁和乐观锁的实现及其局限性。首先介绍了单次访问下库存扣减逻辑的正常运行,但在高并发场景下出现了超卖问题。接着分析了JVM锁在多例模式、事务模式和集群模式下的失效情况,并提出了使用数据库锁机制(如悲观锁和乐观锁)来解决并发问题。 悲观锁通过`update`语句或`select for update`实现,能有效防止超卖,但存在锁范围过大、性能差等问题。乐观锁则通过版本号或时间戳实现,适合读多写少的场景,但也面临高并发写操作性能低和ABA问题。 最终,文章强调没有完美的方案,只有根据具体业务场景选择合适的锁机制。
382 12
【📕分布式锁通关指南 01】从解决库存超卖开始加锁的初体验
|
9月前
|
NoSQL Java Redis
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁
本文详细解析了Redisson可重入锁的加锁流程。首先从`RLock.lock()`方法入手,通过获取当前线程ID并调用`tryAcquire`尝试加锁。若加锁失败,则订阅锁释放通知并循环重试。核心逻辑由Lua脚本实现:检查锁是否存在,若不存在则创建并设置重入次数为1;若存在且为当前线程持有,则重入次数+1。否则返回锁的剩余过期时间。此过程展示了Redisson高效、可靠的分布式锁机制。
317 0
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁

热门文章

最新文章