
本期我们将深入了解 Redisson 提供的另一个常用分布式锁形态——读写锁(ReadWriteLock) 。在实际业务场景中,对于同一关键资源,不同线程可能只需要“读取”或者“写入”,如果能让读操作并行、而写操作互斥,就能大大提升效率。Redisson 为我们提供了一个典型的读写锁实现,让分布式读多写少的场景更具伸缩性。
在分布式的场景下,有些数据是“读远多于写”的。比如一些基础配置、商品信息等,这类场景往往更适合使用读写锁——对于读操作只要不涉及数据修改,就可以同时进行;一旦需要写操作,就要独占保持一致性。Redisson 在此基础之上封装了分布式的读写锁 API,使得我们可以把单机环境下的读写锁模式,透明地延伸到集群。
它的常见应用包括:
接下来让我们一窥源码,看看 RedissonReadWriteLock(以下简称 RWWLock)是如何实现其读写互斥的。
和 JDK 自带的 java.util.concurrent.locks.ReadWriteLock 类似,Redisson 的读写锁同样提供两种锁:
只不过它会分别负责“读 + 读不互斥、写 + 读写互斥”等同步语义,并在底层与 Redis 交互以保证分布式环境的一致性、正确性。
在 Redisson 中,对外暴露的是 RReadWriteLock 接口,后续通过类似来获取读锁或写锁:
RReadWriteLock rwLock = redissonClient.getReadWriteLock("anyRWLock");
RLock readLock = rwLock.readLock();
RLock writeLock = rwLock.writeLock();RedissonReadWriteLock 主要包含以下核心逻辑:
看看 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 时,说明可以彻底释放。
RedissonReadWriteLock 在分布式环境下完美复刻了本地读写锁的行为:读锁可并行、写锁需互斥、读写也互斥。通过 Lua 脚本与 Redis 的结合,它保证了加解锁过程的原子性与正确性,同时在可重入、自动续期等方面做了周全考虑。对于多读少写的分布式业务场景,读写锁可以在并发性能与资源互斥之间取得良好平衡,极大地提升系统吞吐量。
阅读源码能让我们更好地理解这套机制背后的原理与设计,也能帮助我们在日常工作中更加灵活地选型和排查问题。希望本文能给大家带来收获,我们下一期再见!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。