ReentrantLock 与 Redis RLock 对比解读

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(回顾)

  • 必须在 finallyunlock():这是铁律,确保异常时锁也能释放。

  • 警惕锁对象被意外重建 :例如使用 ConcurrentHashMap.computeIfAbsent 动态创建锁时,若 lambda 表达式被并发执行,可能创建出多个锁实例,导致锁失效。

对于 Redisson RLock(重点)

  • 看门狗失效的常见场景

    1. 显式指定了 leaseTime:这是最容易被忽略的一点,一旦指定,看门狗不再续期-。

    2. 客户端进程崩溃或 JVM 长时间 GC:如果 Redisson 客户端进程崩溃,其后台续期线程随之终止,锁会在 30 秒后自动释放,这本身是安全的。但如果是 JVM 因 Full GC 等原因"假死",看门狗线程也被暂停,锁可能超时释放,而业务线程恢复后仍以为自己持有锁,导致并发冲突-。

    3. 解锁失败但看门狗仍在工作 :极端情况下(如网络抖动),unlock() 命令发送失败,但客户端并不知道。如果看门狗仍认为锁被持有并继续续期,就会导致锁被无限期持有,造成"死锁"-。

  • RedLock 的争议:RedLock 算法自提出以来就存在争议。它的安全性依赖于各 Redis 节点的时钟不会发生大的漂移,并且其实现较为复杂,性能开销也更大。对于大多数业务场景,使用单实例或主从架构的 Redisson RLock 配合看门狗机制已经足够。

  • 锁的粒度 :分布式锁的网络开销远大于 JVM 内锁,因此必须尽可能减小锁的粒度。避免用一个大锁保护所有资源,应细分为多个小锁。

💎 总结与选型建议

ReentrantLock 和 Redisson RLock 是解决不同层面问题的工具,不存在简单的替代关系。

  • 选型逻辑非常清晰

    • 如果并发冲突发生在同一个 JVM 进程内 ,使用 ReentrantLock (或 synchronized)。

    • 如果并发冲突发生在多个独立的服务实例之间 ,必须使用 Redisson RLock 这样的分布式锁。

  • 使用 Redisson RLock 的核心原则

    1. 优先使用看门狗模式 :调用 lock()tryLock(等待时间) 而不指定 leaseTime,让 Redisson 自动管理锁的续期,除非你能精确预估业务的最大执行时间。

    2. 务必使用 try-finally 并校验持有者 :在 finally 块中,先通过 lock.isHeldByCurrentThread() 判断当前线程是否仍持有锁,再执行 unlock(),避免误释放其他线程的锁。

    3. 认识到分布式锁的固有风险:网络分区、Redis 故障、客户端假死等情况都可能导致锁的安全性被破坏。分布式锁应该被视为一种"协调"工具,而非绝对安全的保证。在关键业务中,应结合数据库的唯一约束、乐观锁等手段进行最终的数据一致性校验。




ReentrantLock 与 Redis RLock(以 Redisson 实现为代表)虽然都以"可重入"为核心语义,但它们工作在完全不同的抽象层级上:前者是 JVM 进程内的线程同步原语,后者是跨进程、跨节点的分布式互斥协调机制。理解这种层级差异,是正确选型的关键。


一、核心对比总览

对比维度 ReentrantLock(Java) RLock(Redis / Redisson)
作用域 单个 JVM 进程内的线程间互斥 跨 JVM、跨机器、跨网络的分布式互斥
可重入实现载体 AQS 的 volatile int state 计数 Redis Hash 结构中的重入计数字段
锁持有者标识 AbstractOwnableSynchronizerexclusiveOwnerThread 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 中调用

相关推荐
YDS8292 小时前
Small Spring IOC篇:实现 Bean 的定义、注册、获取
java·spring
边境悍匪2 小时前
蜗牛学苑 Java 智能体学习 Day45|Knife4j+SpringBoot3、书城项目整合 MyBatis 思维导图复盘
java·开发语言·vue.js·学习·spring
需要8262 小时前
Arthas 一个命令排查线上问题
java·jvm·spring·spring cloud
Kyrie_kk3 小时前
Java--ProcessBuilder操作系统进程
java·后端
SamDeepThinking3 小时前
关于java final关键字的可见性
java·后端·面试
Wang's Blog3 小时前
Java 项目实战: 外卖平台-MyBatis-Plus分页插件与员工分页查询
java·项目开发
MetaLite3 小时前
全网最好的SpringBoot接口请求对象设计
java·spring boot·后端
蜗牛互联网3 小时前
AI给面试打分不够,求职者更需要可核对的证据
java·人工智能·后端
此时不提桶,更待何时3 小时前
02-05-B-虚拟线程面试与生产事故实战
java·面试