RLock(Redis)通常指的是 Redisson 框架实现的分布式可重入锁。它与 Java 原生的 ReentrantLock 虽然都叫"可重入锁",但设计目标和运行环境截然不同:ReentrantLock 解决的是单个 JVM 进程内的线程互斥问题,而 Redisson RLock 解决的是跨多个 JVM 进程、跨网络的分布式互斥问题-。
🎯 根本定位:JVM 内锁 vs. 分布式锁
| 对比维度 | ReentrantLock (Java) | Redisson RLock (Redis) |
|---|---|---|
| 作用范围 | 单个 JVM 进程内的线程之间 | 跨多个 JVM 进程的分布式节点之间 |
| 底层依赖 | JVM 内部的 AQS (AbstractQueuedSynchronizer) | Redis 服务器(通过网络通信) |
| 性能量级 | 纳秒级(内存 CAS 操作) | 毫秒级(网络 RTT + Redis 命令执行) |
| 核心风险 | 代码逻辑错误导致死锁 | 网络分区、Redis 节点故障、时钟漂移 |
⚙️ 核心机制深度对比
ReentrantLock:基于 AQS 的 JVM 内同步
ReentrantLock 的核心是 AQS 框架。AQS 内部维护了一个 volatile int state 字段和一个 FIFO 的双向等待队列-。
-
加锁 :线程通过 CAS 操作尝试将
state从 0 改为 1。成功则获得锁,并将自己设置为独占线程;失败则被包装成 Node 节点加入等待队列并阻塞-。 -
可重入 :已持有锁的线程再次获取锁时,只需将
state递增,无需进入队列。 -
释放 :
state递减,直到归零时才真正释放锁,并唤醒等待队列的队首线程。
Redisson RLock:基于 Redis 的分布式协调
Redisson RLock 利用 Redis 的 Hash 结构和 Lua 脚本的原子性来实现分布式可重入。
-
加锁 :通过一段 Lua 脚本,判断锁 Key 是否存在。若不存在,则用
hset创建 Hash,记录"线程标识"和重入次数(初始为 1),并设置过期时间;若存在且持有者是自己,则用hincrby将重入次数加 1-。 -
可重入:依靠 Hash 结构中记录的"持有者线程标识"和"重入计数"来判断。只有当前持有该锁的线程(或客户端实例)才能进行重入操作-。
-
解锁:同样通过 Lua 脚本,先校验当前线程是否持有锁,然后对重入次数减 1。只有当计数归零时,才真正删除 Key 并发布消息唤醒等待的客户端-。
✨ 关键特性对比
看门狗机制:Redisson 的独有设计
这是 Redisson RLock 最重要的特性之一。当业务执行时间不确定时,手动设置锁超时时间非常困难,设置过短会导致业务未完成锁就失效,过长则可能在客户端崩溃后长时间阻塞其他进程。
看门狗机制正是为了解决这个问题。当你调用 lock.lock() 而不指定 leaseTime 时 ,Redisson 会启动一个后台定时任务-1。
-
初始租约 :锁的初始过期时间默认为 30 秒 -1。
-
续期频率 :看门狗会每隔租约时间的 1/3(即 10 秒 )检查一次,如果业务线程仍持有锁,就将过期时间重置回 30 秒-。
-
终止条件 :只要业务线程没有显式调用
unlock(),且客户端进程未崩溃,续期就会一直进行-1。 -
重要限制 :一旦你显式指定了
leaseTime(如lock.lock(10, TimeUnit.SECONDS)),看门狗机制就会自动失效,锁将在指定时间后强制释放-。
公平性支持
-
ReentrantLock:支持公平与非公平模式。公平模式下,锁会严格按照等待队列的 FIFO 顺序分配,避免线程饥饿,但吞吐量会下降。
-
Redisson RLock :基础的
RLock是非公平的。如果需要公平锁,需要使用redisson.getFairLock()。其实现是在 Redis 中使用一个 ZSet(有序集合)来维护等待队列,ZSet 的 score 通常是请求时间戳,确保先到先得-。
高级锁变体
Redisson 还提供了一些 ReentrantLock 不具备的、针对分布式场景的锁变体:
-
MultiLock(联锁) :可以将多个独立的 RLock 对象关联为一个逻辑锁。只有当所有子锁都加锁成功时,才算获取成功;任何一个失败,已获取的子锁都会回滚释放-。
-
RedLock(红锁) :这是 Redis 作者提出的算法。它要求客户端向 N 个完全独立的 Redis 主节点 (通常 N=5)依次申请锁,只有在大多数节点(N/2+1) 上都成功获取锁,且总耗时小于锁的有效期,才认为加锁成功。其目的是为了在部分 Redis 节点故障时,依然能保证锁的安全性-32。
💻 实战场景与代码示例
Redisson RLock 典型场景
场景一:分布式定时任务防重复执行
在多实例部署的服务中,需要确保同一个定时任务同一时间只被一个实例执行。此时业务执行时间可能因数据量而变化,适合启用看门狗。
@Service
public class DistributedJobService {
@Autowired
private RedissonClient redissonClient;
public void executeJob() {
RLock lock = redissonClient.getLock("job:syncData");
// 不指定 leaseTime,启用看门狗自动续期
// tryLock 在 0 秒内尝试获取,获取不到立即返回 false
boolean isLocked = lock.tryLock();
if (!isLocked) {
log.info("任务已被其他实例执行,跳过。");
return;
}
try {
// 执行可能耗时不确定的业务逻辑
doSyncData();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
场景二:RedLock 用于高安全性要求场景
在金融交易等对锁安全性要求极高的场景,单 Redis 实例存在主从切换时锁丢失的风险,可以使用 RedLock。
// 假设有三个独立的 RedissonClient 实例,连接到三个独立的 Redis 主节点
RLock lock1 = client1.getLock("redlock:order:123");
RLock lock2 = client2.getLock("redlock:order:123");
RLock lock3 = client3.getLock("redlock:order:123");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
// 尝试获取红锁,最多等待 3 秒,锁自动释放时间 10 秒
boolean res = redLock.tryLock(3, 10, TimeUnit.SECONDS);
if (res) {
// 执行业务逻辑
}
} finally {
redLock.unlock();
}
⚠️ 应用注意事项与常见陷阱
对于 ReentrantLock(回顾)
-
必须在
finally中unlock():这是铁律,确保异常时锁也能释放。 -
警惕锁对象被意外重建 :例如使用
ConcurrentHashMap.computeIfAbsent动态创建锁时,若 lambda 表达式被并发执行,可能创建出多个锁实例,导致锁失效。
对于 Redisson RLock(重点)
-
看门狗失效的常见场景:
-
显式指定了
leaseTime:这是最容易被忽略的一点,一旦指定,看门狗不再续期-。 -
客户端进程崩溃或 JVM 长时间 GC:如果 Redisson 客户端进程崩溃,其后台续期线程随之终止,锁会在 30 秒后自动释放,这本身是安全的。但如果是 JVM 因 Full GC 等原因"假死",看门狗线程也被暂停,锁可能超时释放,而业务线程恢复后仍以为自己持有锁,导致并发冲突-。
-
解锁失败但看门狗仍在工作 :极端情况下(如网络抖动),
unlock()命令发送失败,但客户端并不知道。如果看门狗仍认为锁被持有并继续续期,就会导致锁被无限期持有,造成"死锁"-。
-
-
RedLock 的争议:RedLock 算法自提出以来就存在争议。它的安全性依赖于各 Redis 节点的时钟不会发生大的漂移,并且其实现较为复杂,性能开销也更大。对于大多数业务场景,使用单实例或主从架构的 Redisson RLock 配合看门狗机制已经足够。
-
锁的粒度 :分布式锁的网络开销远大于 JVM 内锁,因此必须尽可能减小锁的粒度。避免用一个大锁保护所有资源,应细分为多个小锁。
💎 总结与选型建议
ReentrantLock 和 Redisson RLock 是解决不同层面问题的工具,不存在简单的替代关系。
-
选型逻辑非常清晰:
-
如果并发冲突发生在同一个 JVM 进程内 ,使用 ReentrantLock (或
synchronized)。 -
如果并发冲突发生在多个独立的服务实例之间 ,必须使用 Redisson RLock 这样的分布式锁。
-
-
使用 Redisson RLock 的核心原则:
-
优先使用看门狗模式 :调用
lock()或tryLock(等待时间)而不指定leaseTime,让 Redisson 自动管理锁的续期,除非你能精确预估业务的最大执行时间。 -
务必使用
try-finally并校验持有者 :在finally块中,先通过lock.isHeldByCurrentThread()判断当前线程是否仍持有锁,再执行unlock(),避免误释放其他线程的锁。 -
认识到分布式锁的固有风险:网络分区、Redis 故障、客户端假死等情况都可能导致锁的安全性被破坏。分布式锁应该被视为一种"协调"工具,而非绝对安全的保证。在关键业务中,应结合数据库的唯一约束、乐观锁等手段进行最终的数据一致性校验。
-
ReentrantLock 与 Redis RLock(以 Redisson 实现为代表)虽然都以"可重入"为核心语义,但它们工作在完全不同的抽象层级上:前者是 JVM 进程内的线程同步原语,后者是跨进程、跨节点的分布式互斥协调机制。理解这种层级差异,是正确选型的关键。
一、核心对比总览
| 对比维度 | ReentrantLock(Java) | RLock(Redis / Redisson) |
|---|---|---|
| 作用域 | 单个 JVM 进程内的线程间互斥 | 跨 JVM、跨机器、跨网络的分布式互斥 |
| 可重入实现载体 | AQS 的 volatile int state 计数 |
Redis Hash 结构中的重入计数字段 |
| 锁持有者标识 | AbstractOwnableSynchronizer 的 exclusiveOwnerThread |
Hash 中的线程标识(UUID + threadId) |
| 公平性 | 支持公平/非公平两种模式 | Redisson 默认非公平,无原生公平锁实现 |
| 锁自动释放 | 无(必须显式 unlock()) |
有 TTL 兜底;看门狗机制支持自动续期 |
| 故障模型 | 线程异常/死锁 → 锁永久持有,需人工干预 | 客户端宕机 → 锁在 TTL 后自动释放 |
| 等待唤醒机制 | AQS 的 CLH 队列 + LockSupport.park/unpark |
Redis Pub/Sub 订阅释放信号 + 自旋重试 |
| 释放安全性 | 仅持有线程可释放(exclusiveOwnerThread 校验) |
Lua 脚本校验 Hash 中的线程标识后原子释放 |
| 原子性保障 | CAS + volatile 读写 |
Lua 脚本在 Redis 单线程中原子执行 |
二、ReentrantLock 实现机制深入
ReentrantLock 的可重入能力根植于 AQS 的 volatile int state 字段。state 为 0 表示锁空闲;线程获取锁时 state 加 1,同一线程再次获取时继续递增;释放时递减,减至 0 才真正释放锁-18。
公平性的实现差异 体现在两个内部类上。NonfairSync.lock() 首先直接尝试 CAS 将 state 从 0 改为 1,成功则立即持有锁,完全无视队列中的等待者;失败才进入 acquire() 流程。FairSync.lock() 则直接调用 acquire(),在 tryAcquire 中额外执行 hasQueuedPredecessors() 判断------只有当前线程是等待队列的队首(或队列为空)时,才允许尝试获取锁-18。
等待与唤醒 依赖 AQS 的 CLH 双向队列。获取失败的线程被封装为 Node 节点入队,通过 LockSupport.park() 阻塞。前驱节点释放锁后,调用 unpark() 唤醒后继节点重新竞争。这一机制完全在 JVM 用户态完成,不涉及任何网络或序列化开销。
故障模型的关键特征 :ReentrantLock 没有 TTL 概念。如果持有锁的线程发生死循环、长时间 GC 或死锁,锁将永久被持有,其他线程无限阻塞。这也是为什么必须使用 try...finally 确保 unlock() 被调用。
三、Redis RLock 实现机制深入
Redisson 的 RLock 将可重入语义映射到 Redis 的 Hash 数据结构上。加锁时执行的 Lua 脚本逻辑为:若锁 Key 不存在,使用 hset 写入 {threadId: 1} 并设置 pexpire;若锁已存在且 Hash 中的 threadId 与当前线程一致,则将重入计数加 1 并刷新过期时间-1。
看门狗机制 是 Redisson 区别于原生 Redis 锁的核心能力。当调用无参的 lock() 方法时,Redisson 使用默认 30 秒的 lockWatchdogTimeout,并在锁获取成功后启动一个后台定时任务,每隔超时时间的 1/3(默认 10 秒)通过 Lua 脚本将锁的过期时间重置为 30 秒-11。该任务递归调度自身,只要锁仍被持有且客户端存活,续期就不会停止-11。
等待与唤醒 机制与 ReentrantLock 截然不同。获取锁失败后,Redisson 不会立即重试,而是通过 Redis 的 Pub/Sub 订阅锁释放消息。当持有者释放锁时,Lua 脚本在删除锁后会 publish 一条释放通知,等待中的客户端通过订阅收到信号后再尝试获取-1。这种设计避免了纯粹的忙等待对 Redis 造成不必要的压力。
故障模型是两者最本质的差异所在。Redis RLock 必须面对分布式系统特有的失效场景:
-
主从切换丢锁:Redis 主从复制是异步的。客户端在主节点成功加锁后,若主节点在锁数据同步到从节点之前宕机,从节点晋升为新主节点时锁并不存在,另一个客户端就可以同时获得锁-。Redlock 算法通过向 N 个独立 Redis 实例(通常为 5 个)申请锁、超过半数成功才认为加锁成功来缓解此问题,但这增加了显著的网络往返开销和实现复杂度-。
-
看门狗失效 :若在调用
lock()时显式指定了leaseTime(如lock.lock(10, TimeUnit.SECONDS)),看门狗机制会被完全禁用 ,锁将在 leaseTime 到期后自动释放,无论业务是否完成-35。 -
事务嵌套导致提前释放 :在 Spring
@Transactional方法内加锁,若在finally中释放锁,而事务尚未提交,其他线程可能读到未提交的中间状态数据。锁的释放时机早于事务提交时机,破坏了互斥语义-35。 -
网络抖动或客户端资源不足 :续期请求因网络故障未到达 Redis,或客户端 CPU/内存耗尽导致看门狗线程无法调度,锁都可能提前过期-35。
四、实战场景与选型决策
ReentrantLock 的适用边界:当并发控制的范围严格限定在单个 JVM 进程内时,ReentrantLock 是更优选择。它的性能开销远低于任何分布式锁------后者涉及网络 I/O、序列化、Redis 命令执行等多个环节-。需要公平调度、超时获取、中断响应或多条件变量协作的场景,也应优先使用 ReentrantLock。
Redis RLock 的适用边界 :当应用部署为多节点,本地锁无法跨节点生效时,Redis RLock 是自然选择。典型场景包括:电商库存扣减需要跨订单服务节点互斥、定时任务需要集群中仅一个节点执行、分布式资源分配需要全局唯一持有者-28。
一个容易被忽视的实践原则 :不要用分布式锁替代良好的架构设计。如果业务逻辑可以通过数据库唯一约束、消息队列幂等消费或状态机等机制天然避免并发冲突,引入分布式锁只会增加系统复杂度和故障面。Redis RLock 的看门狗、Pub/Sub、Lua 脚本等机制虽然精巧,但它们都是在弥补分布式环境下"缺乏共享内存"这一根本约束。本地能解决的问题,不应上升到分布式层面。
选型决策树:
-
并发控制范围在单 JVM 内 → ReentrantLock(或 synchronized,若无需高级特性)
-
需要跨节点互斥,且能接受 Redis 作为基础设施 → Redis RLock(Redisson)
-
需要跨节点互斥,且对锁安全性有极端要求(不能容忍任何丢锁) → 评估 ZooKeeper 或 etcd,它们基于共识协议,不存在异步复制丢锁问题
-
使用 Redis RLock 时,若业务执行时间确定且较短 ,显式指定
leaseTime可避免看门狗线程开销;若时间不确定 ,使用无参lock()启用看门狗,并确保unlock()在finally中调用