Redisson 分布式锁:普通锁、公平锁与读写锁
一、三种锁的区别
Redisson 提供多种分布式锁,常见的有普通锁、公平锁和读写锁。
三种锁都支持可重入,未指定固定 leaseTime 时,都可以使用 Watch Dog 自动续期。
二、普通锁 RLock
1. 基本原理
普通锁是一种非公平、可重入的独占锁。
同一时刻,只允许一个线程持有同一把锁。
无论执行读取还是修改操作,都需要等待锁释放。
不保证线程按照申请锁的顺序获取锁。
同一线程可以重复获取自己持有的锁。
例如线程 A、B、C 同时竞争:
申请顺序:A → B → C
可能的获取顺序:A → C → B2. Java 使用
RLock lock = redissonClient.getLock("lock:order:1001");
boolean acquired = false;
try {
// 最多等待3秒,启用看门狗
acquired = lock.tryLock(3, TimeUnit.SECONDS);
if (!acquired) {
throw new RuntimeException("获取锁失败");
}
// 执行业务逻辑
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁被中断", e);
} finally {
if (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}普通锁适合大部分需要互斥执行的业务,是最常用的 Redisson 分布式锁。
三、公平锁 FairLock
1. 基本原理
公平锁和普通锁一样,都属于独占锁。
区别在于:
公平锁按照线程进入锁等待队列的顺序获取锁(FIFO,先进先出)。
例如:
线程 A 申请锁
线程 B 申请锁
线程 C 申请锁
等待队列:A → B → C
获取顺序:A → B → C需要注意,这里的先后顺序是进入 Redis 锁等待队列的顺序,而不是 Java 线程创建顺序。
2. Java 使用
RLock lock = redissonClient.getFairLock("lock:order:1001");
boolean acquired = false;
try {
acquired = lock.tryLock(3, TimeUnit.SECONDS);
if (!acquired) {
throw new RuntimeException("获取公平锁失败");
}
// 执行业务逻辑
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁被中断", e);
} finally {
if (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}使用方式与普通锁基本一致,只需要将 getLock() 换成 getFairLock()。
3. 底层原理
公平锁通过 Redis 维护等待线程的顺序,核心涉及:
Hash:记录持锁线程和重入次数。
List:维护等待队列。
Sorted Set:辅助记录等待超时信息。
Lua:保证队列和锁操作的原子性。
Pub/Sub:通知等待线程重新竞争锁。
公平锁的缺点:
需要额外维护等待队列,开销通常高于普通锁。
如果队列中存在失效线程,可能造成额外等待。
无法保证业务请求严格按照创建时间处理,只保证锁等待队列层面的顺序。
因此,一般业务优先使用普通锁,只有明确要求按锁申请顺序执行时,才考虑公平锁。
四、读写锁 ReadWriteLock
1. 基本原理
读写锁并不是三种锁,而是由两种锁组成:
读锁(ReadLock):共享锁,允许多个线程同时读取。
写锁(WriteLock):独占锁,不允许其他线程同时持有冲突的读锁或写锁。
读写锁有三个核心规则:
读读共享、读写互斥、写写互斥。
2. 读读共享
两个线程都只需要读取数据,可以同时获取读锁。
线程 A → 获取读锁 → 读取配置
线程 B → 获取读锁 → 读取配置
两个线程可以同时执行例如:
RReadWriteLock rwLock =
redissonClient.getReadWriteLock("lock:config");
RLock readLock = rwLock.readLock();
readLock.lock();
try {
// 查询配置
} finally {
readLock.unlock();
}3. 读写互斥
线程 A 正在读取数据,线程 B 想修改数据。
线程 A → 获取读锁 → 查询数据
线程 B → 申请写锁 → 等待
线程 A 释放读锁
↓
线程 B 才能获取写锁反过来也一样:当某个线程持有写锁时,其他线程无法获取冲突的读锁。
4. 写写互斥
两个线程都需要修改同一份数据。
线程 A → 获取写锁 → 修改数据
线程 B → 申请写锁 → 等待
线程 A 释放写锁
↓
线程 B 获取写锁写操作必须独占,避免多个线程同时修改共享资源。
5. Java 使用
RReadWriteLock rwLock =
redissonClient.getReadWriteLock("lock:config");
// 获取读锁
RLock readLock = rwLock.readLock();
// 获取写锁
RLock writeLock = rwLock.writeLock();查询操作:
readLock.lock();
try {
// 查询数据
} finally {
readLock.unlock();
}修改操作:
writeLock.lock();
try {
// 修改数据
} finally {
writeLock.unlock();
}实际项目中可以使用带等待时间的 tryLock(),避免长时间阻塞。
五、普通锁与读写锁的区别
普通锁在互斥效果上,可以理解为读写锁中的写锁。
但两者并不完全相同。
例如有100个线程查询同一个门店配置:
使用普通锁:
线程 A 查询 → 释放锁
线程 B 查询 → 释放锁
线程 C 查询 → 释放锁
……同一时间只能有一个线程持锁。
使用读写锁:
线程 A 查询 ─────┐
线程 B 查询 ─────┼── 可以并发执行
线程 C 查询 ─────┘
线程 D 修改 → 等待读锁释放因此,读写锁可以减少纯读取操作之间不必要的互斥等待。
六、实际开发注意事项
普通业务优先使用 RLock,不需要为了公平性而使用 FairLock。
读写锁适用于读多写少、确实需要跨实例协调的共享资源,并非查询数据库就必须加读锁。
读锁不能随意升级为写锁,否则可能发生等待甚至死锁。
读写锁不会自动识别 SQL 类型,必须由开发者保证读取使用读锁、修改使用写锁。
锁要覆盖数据库事务提交,避免释放锁后事务仍未提交。
不要混用普通锁和读写锁来协调同一业务资源,应统一锁协议。
读写锁并不能替代数据库事务或条件更新,重要数据仍需数据库层面的保护。
七、总结
1. 普通锁和公平锁有什么区别?
普通锁属于非公平的独占锁,不保证线程获取锁的顺序。公平锁通过维护等待队列,实现 FIFO 获取顺序,但需要额外维护队列,性能开销通常更高。
2. 读写锁是什么?
读写锁由读锁和写锁组成。读锁是共享锁,允许多个线程同时读取;写锁是独占锁,与其他线程的读锁和写锁互斥。
3. 普通锁和读写锁有什么区别?
普通锁无论读写操作都需要独占锁;读写锁允许多个读线程并发持锁,因此更适合需要协调读多写少的共享资源。
4. 普通锁是否相当于读写锁中的写锁?
从不同线程之间的互斥效果来看,可以这样理解。但普通锁是一套独立的互斥锁机制,而写锁属于读写锁的一部分,必须与同一组读锁协调。
八、核心记忆
普通锁 RLock:独占、非公平、可重入,适合一般互斥业务。
公平锁 FairLock:独占、FIFO,适合需要按锁等待顺序处理的业务。
读写锁 ReadWriteLock:读读共享、读写互斥、写写互斥,适合读多写少且需要协调的共享资源。
记住一句话:普通锁解决互斥问题,公平锁在互斥基础上解决排队顺序问题,读写锁在保证写操作互斥的前提下提高读取并发能力。
