引言
本期我们将深入了解 Redisson 提供的另一个常用分布式锁形态——读写锁(ReadWriteLock) 。在实际业务场景中,对于同一关键资源,不同线程可能只需要“读取”或者“写入”,如果能让读操作并行、而写操作互斥,就能大大提升效率。Redisson 为我们提供了一个典型的读写锁实现,让分布式读多写少的场景更具伸缩性。
介绍
在分布式的场景下,有些数据是“读远多于写”的。比如一些基础配置、商品信息等,这类场景往往更适合使用读写锁——对于读操作只要不涉及数据修改,就可以同时进行;一旦需要写操作,就要独占保持一致性。Redisson 在此基础之上封装了分布式的读写锁 API,使得我们可以把单机环境下的读写锁模式,透明地延伸到集群。
它的常见应用包括:
- 分布式缓存的配置数据:读操作要并发读,而更新操作需要阻塞所有读操作。
- 某些共享资源的高并发访问场景:比如日志、统计信息等,与写操作互斥而与读操作并行。
接下来让我们一窥源码,看看 RedissonReadWriteLock(以下简称 RWWLock)是如何实现其读写互斥的。
读写锁的实现思路
和 JDK 自带的 java.util.concurrent.locks.ReadWriteLock 类似,Redisson 的读写锁同样提供两种锁:
- 读锁(RLock 类型)
- 写锁(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。随后,它会分别返回 RedissonReadLock 或 RedissonWriteLock 来完成具体的分布式加解锁逻辑。接下来,让我们聚焦更细节的 RedissonReadLock 和 RedissonWriteLock 实现。
读锁
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 时,说明可以彻底释放。
如何保证分布式读写锁的一致性?
- Lua 脚本原子性
Redisson 将所有加锁、解锁、自增或自减计数等动作打包到 Lua 脚本中,并在 Redis 服务器端原子执行,避免了“网络传输过程中分步执行可能导致的并发问题”。 - 分隔的 key 空间
读锁与写锁在 Redis 中对应不同的 key,避免互相冲突同时也便于统计读锁计数。但在加锁的 Lua 脚本中,会先检查写锁 key、再检查读锁 key,或反之,以此维持正确关系。 - 可重入语义
当同一线程再次获取读锁或写锁时,Redis 中的计数会递增,继续维持对资源的持有,这对于分布式场景下的可重入需求非常重要。 - 自动过期
和其他 Redisson 锁类似,读写锁也提供 leaseTime 等机制,让锁可以在指定时间后自动释放,防止意外死锁。
小结
RedissonReadWriteLock 在分布式环境下完美复刻了本地读写锁的行为:读锁可并行、写锁需互斥、读写也互斥。通过 Lua 脚本与 Redis 的结合,它保证了加解锁过程的原子性与正确性,同时在可重入、自动续期等方面做了周全考虑。对于多读少写的分布式业务场景,读写锁可以在并发性能与资源互斥之间取得良好平衡,极大地提升系统吞吐量。
阅读源码能让我们更好地理解这套机制背后的原理与设计,也能帮助我们在日常工作中更加灵活地选型和排查问题。希望本文能给大家带来收获,我们下一期再见!