一、引言:分布式锁的前世今生
在单体架构时代,我们在 Java 并发编程中使用的锁(如synchronized、ReentrantLock)都是在同一个 JVM 进程内生效的。锁的标记存储在 JVM 内存中,线程之间通过共享内存来协调对临界资源的访问。然而,随着微服务架构的普及,同一个应用往往部署在多个节点上,此时 JVM 级别的锁就无能为力了——因为它们无法跨 JVM 进程进行协调。
举个典型的电商场景:假设我们有一个扣减库存的接口,用户下单时需要将商品库存减 1。如果该服务部署了 3 个节点,当两个用户同时购买同一件商品时,请求可能被负载均衡分发到不同的节点上。如果没有分布式锁,两个节点上的线程可能同时读取到库存为 10,都执行减 1 操作后写入 9,导致实际上只减了一次库存,出现超卖问题。
为了在分布式环境中保证数据一致性,分布式锁应运而生。分布式锁是一种跨 JVM 的互斥机制,它利用一个所有节点都能访问的"公共存储"来标记锁状态,从而协调多个进程对共享资源的访问。常见的实现方式有三种:
- 基于数据库:利用唯一索引约束或行级锁来实现,简单但性能较差,且存在锁无法自动释放的问题。
- 基于 ZooKeeper:利用临时顺序节点和 Watcher 机制实现,可靠性高但性能一般,且强依赖 ZooKeeper 集群。
- 基于 Redis:利用 Redis 的原子命令(如 SETNX)和过期时间机制实现,性能高,部署简单,是目前业界最主流的分布式锁方案。
在 Redis 生态中,Redisson是最强大的分布式锁实现框架。它不仅提供了基本的互斥锁,还提供了可重入锁、公平锁、联锁、红锁、读写锁、信号量等一系列高级功能。本文作为系列的第一篇,将聚焦于 Redisson 中最核心的可重入同步锁(Reentrant Lock),从架构设计、源码实现到底层原理,进行深度解析。
二、Redisson 框架概述
2.1 什么是 Redisson
Redisson 是一个在 Redis 基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列分布式的 Java 常用对象,还提供了许多分布式服务。其中,分布式锁是 Redisson 最广为人知的功能之一。
Redisson 的核心理念是:将 Redis 作为分布式协调的中间件,通过 Lua 脚本保证操作的原子性,在客户端侧实现丰富的分布式数据结构和服务。
2.2 Redisson 的整体架构
Redisson 的架构可以抽象为以下几个层次:
- 连接层:基于 Netty 实现的高性能异步网络通信,支持单节点、主从、哨兵和集群模式。通过
RedisClient和RedisConnection管理 Redis 连接池。 - 命令层:封装了所有 Redis 命令的异步执行逻辑,通过
RedisCommandExecutor统一调度,支持同步、异步和响应式三种调用方式。 - 数据结构层:提供了丰富的分布式对象,如 RMap、RList、RAtomicLong、RBitSet 等,以及分布式锁、计数信号量、布隆过滤器等高级服务。
- 服务层:提供分布式锁(RLock)、分布式执行器(RExecutorService)、分布式远程服务(RRemoteService)等高阶功能。
在分布式锁的实现中,Redisson 使用了Redis 的 Hash 数据结构来存储锁信息,结合Lua 脚本保证加锁和释放锁的原子性,并创新性地引入了看门狗(Watchdog)机制实现锁的自动续期。
三、可重入锁的核心概念
3.1 什么是可重入性
可重入(Reentrant)是指同一个线程在外层方法获取锁以后,在进入内层方法时,可以自动再次获取同一把锁,而不会被阻塞。换句话说,锁的持有线程可以多次进入被该锁保护的代码块。
举个例子:
public class ReentrantExample { private final RLock lock = redisson.getLock("myLock"); public void methodA() { lock.lock(); try { System.out.println("进入 methodA"); methodB(); // 调用 methodB,methodB 也需要同一把锁 } finally { lock.unlock(); } } public void methodB() { lock.lock(); // 同一个线程再次获取同一把锁,不会被阻塞 try { System.out.println("进入 methodB"); } finally { lock.unlock(); } } }如果锁不具备可重入性,上面的代码就会发生死锁:methodA获取锁后调用methodB,methodB尝试再次获取同一把锁时会被阻塞,而methodA又在等待methodB返回,形成死锁。
3.2 可重入锁的实现原理
在单机环境下,Java 的ReentrantLock通过 AQS(AbstractQueuedSynchronizer)中的state变量来记录重入次数。每次lock()时,如果当前线程已经是锁持有者,state加 1;每次unlock()时,state减 1;当state减到 0 时,锁才真正被释放。
在 Redis 分布式环境中,Redisson 借鉴了这一思想,但通过 Redis 的 Hash 数据结构来实现。具体来说:
- 外层 Key:锁的名称(如
myLock),作为 Redis Hash 的 Key。 - 内层 Key:当前持有锁的线程标识(UUID + 线程 ID),作为 Hash 中的 field。
- 内层 Value:重入次数,作为 Hash 中 field 对应的 value。
每次同一个线程重入时,将对应的 value 加 1;每次释放时,将 value 减 1;当 value 减到 0 时,删除该 field,如果 Hash 为空则删除整个 Key。
四、Redisson 可重入锁的架构设计
4.1 类层次结构
Redisson 可重入锁的类设计遵循了清晰的职责分离原则。核心类包括:
RLock(接口):继承了java.util.concurrent.locks.Lock和RLockAsync,定义了标准的分布式锁操作。它提供了同步和异步两种调用方式,还扩展了tryLock(long waitTime, long leaseTime, TimeUnit unit)等带超时参数的方法。RedissonLock(核心实现类):实现了RLock接口,是 Redisson 可重入锁的核心实现。它包含了加锁、释放锁、续期等核心逻辑,并持有CommandAsyncExecutor用于执行 Redis 命令。该类还维护了id(UUID 作为客户端唯一标识)和internalLockLeaseTime(默认锁租约时间,30 秒)等关键字段。RedissonBaseLock(抽象基类):提供了锁的公共基础实现,包括线程 ID 获取、过期时间刷新等工具方法。它继承自RedissonExpirable,后者提供了 Redis Key 过期相关的基础功能。RedissonLockEntry(内部数据结构):表示一个锁的持有者信息,包含客户端 ID 和重入计数器。LockPubSub(发布订阅机制):基于 Redis 的发布订阅功能,实现锁释放通知。当锁被释放时,持有锁的客户端会发布消息,通知等待队列中的其他客户端可以尝试获取锁。
4.2 核心数据结构设计
Redisson 可重入锁在 Redis 中的存储结构如下:
Key: myLock (锁名称) Type: Hash Field (Hash Key): "a1b2c3d4-e5f6-7890-abcd-ef1234567890:1" ├── UUID 部分: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" (客户端唯一标识) └── 线程 ID 部分: "1" (当前线程 ID) Value: 2 (重入次数)为什么使用 UUID + 线程 ID 的组合作为 field?
- UUID 区分不同客户端:在分布式环境中,不同 JVM 进程中的线程 ID 可能重复(比如都是线程 1),所以需要 UUID 来唯一标识客户端。
- 线程 ID 区分同一客户端的线程:同一个 JVM 中的不同线程绝不能共享锁,所以需要线程 ID 来区分。
- 重入次数记录重入深度:同一个线程可以多次获取同一把锁,每次重入重入次数加 1。
这种设计的优势在于:
- 通过 Hash 数据结构,一个 Key 可以存储多个客户端的信息(虽然同一时刻只有一个客户端持有锁,但可以记录历史持有者)。
- 通过 field 的命名规则,可以精确判断锁持有者身份,防止误释放。
- 重入次数的独立存储使得锁的释放逻辑清晰简单。
4.3 锁的生命周期状态机
Redisson 可重入锁在整个生命周期中会经历以下状态转换:
- 空闲状态(Free):锁未被任何线程持有,Redis 中不存在对应的 Key。此时任何线程都可以尝试获取锁。
- 持有状态(Held):某个线程成功获取锁,Redis 中创建对应 Key 的 Hash 结构,重入次数为 1。看门狗机制启动,定期续期。
- 重入状态(Reentered):同一个线程再次获取同一把锁,Hash 中对应 field 的 value 加 1。看门狗继续运行,保持锁的有效性。
- 部分释放状态(Partially Released):线程调用 unlock(),Hash 中对应 field 的 value 减 1,但不为 0。锁仍然有效,其他线程无法获取。
- 完全释放状态(Fully Released):线程调用 unlock() 使得 value 减到 0,删除该 field。如果 Hash 为空,则删除整个 Key。锁重新回到空闲状态,通过 PubSub 通知等待的线程。
- 过期状态(Expired):如果持有锁的客户端崩溃或网络中断,看门狗无法续期,锁在租约过期后自动释放。Redis 自动删除 Key,锁回到空闲状态。
五、加锁流程深度剖析
5.1 加锁入口方法
Redisson 可重入锁的加锁入口是RedissonLock.lock()方法。该方法支持以下几种调用方式:
lock():无参加锁,使用默认租约时间(30 秒),看门狗自动续期。如果没有指定 leaseTime,锁将一直持有直到显式释放。lock(long leaseTime, TimeUnit unit):指定租约时间的加锁,到期自动释放,不会启动看门狗。适用于可以预估业务执行时间的场景。tryLock():尝试加锁,立即返回结果,不会阻塞等待。tryLock(long waitTime, long leaseTime, TimeUnit unit):在指定时间内尝试加锁,超时返回 false。这个方法是实际最常用的方式。
以下是lock()方法的核心调用链:
// RedissonLock.java @Override public void lock() { try { lock(-1, null, false); } catch (InterruptedException e) { throw new IllegalStateException(); } } private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException { // 1. 获取当前线程 ID long threadId = Thread.currentThread().getId(); // 2. 尝试获取锁 Long ttl = tryAcquire(-1, leaseTime, unit, threadId); if (ttl == null) { // 获取锁成功,直接返回 return; } // 3. 获取锁失败,进入等待-通知循环 // ... 订阅锁释放通知,阻塞等待 }5.2 核心 Lua 脚本:加锁逻辑
加锁的核心逻辑通过tryAcquire()->tryAcquireAsync()->tryLockInnerAsync()这条调用链,最终执行 Lua 脚本。这是整个 Redisson 可重入锁最核心的代码:
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command, // Lua 脚本开始 "if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " + "return redis.call('pttl', KEYS[1]);", // Lua 脚本结束 Collections.singletonList(getRawName()), // KEYS[1]: 锁名称 unit.toMillis(leaseTime), // ARGV[1]: 锁租约时间 getLockName(threadId) // ARGV[2]: UUID:线程ID ); }这个 Lua 脚本的逻辑非常清晰,分为三个分支:
分支一:锁不存在(第一次获取)
if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;当 Redis 中不存在锁对应的 Key 时,说明锁当前是空闲的。执行以下操作:
hincrby KEYS[1] ARGV[2] 1:在 Hash 中设置 field 为客户端标识,value 为 1。由于 Key 之前不存在,Redis 会自动创建 Hash 结构。pexpire KEYS[1] ARGV[1]:设置锁的过期时间,防止客户端崩溃后锁永远无法释放。return nil:返回 nil 表示获取锁成功。
分支二:锁存在且是当前线程持有(可重入)
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;当锁存在且 Hash 中已存在当前客户端的 field,说明是同一个线程重入:
hincrby KEYS[1] ARGV[2] 1:将重入次数加 1。pexpire KEYS[1] ARGV[1]:刷新过期时间,防止锁意外过期。return nil:返回 nil 表示重入成功。
分支三:锁被其他线程持有(获取失败)
return redis.call('pttl', KEYS[1]);当前面的条件都不满足时,说明锁被其他线程持有,返回锁的剩余过期时间(TTL)。这个 TTL 值会被用于后续的等待策略:客户端会订阅这个锁的释放事件,当锁被释放时收到通知,或者等待 TTL 时间后再次尝试。
5.3 等待与重试机制
当加锁失败(返回了 TTL 而非 nil)时,Redisson 不会简单地轮询重试,而是采用了一套精巧的发布订阅 + 信号量机制:
// 简化后的等待逻辑 while (true) { // 1. 尝试获取锁 Long ttl = tryAcquire(-1, leaseTime, unit, threadId); if (ttl == null) { break; // 获取成功 } // 2. 订阅锁释放通知 RFuture<RedissonLockEntry> future = subscribe(threadId); // 3. 等待(带超时) try { // 使用信号量等待,等待时间为 TTL future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); } finally { // 4. 取消订阅 unsubscribe(future, threadId); } }这段逻辑的关键点:
- 发布订阅通知:当锁被释放时,持有者会通过 Redis PubSub 发布消息,所有等待该锁的客户端都会收到通知,唤醒等待的线程。
- 信号量(Semaphore):每个客户端在等待时都有一个信号量,
tryAcquire(ttl, TimeUnit.MILLISECONDS)表示最多等待 TTL 时间。如果在 TTL 时间内收到释放通知,信号量被释放,线程立即唤醒再次尝试获取锁。 - 避免惊群效应:收到通知后,所有等待的线程都会被唤醒,但只有一个能成功获取锁(因为 Lua 脚本是原子的),其他线程会再次进入等待状态。虽然不是完全避免惊群,但由于使用了信号量超时机制,整体性能依然可控。
5.4 加锁流程图
flowchart TD A[调用 lock] --> B[执行 Lua 脚本] B --> C{锁是否存在?} C -->|不存在| D[hincrby 设置 field=1] D --> E[pexpire 设置过期时间] E --> F[返回 nil] F --> G[加锁成功] G --> H[启动看门狗续期] C -->|存在| I{当前线程是否持有?} I -->|是| J[hincrby 重入次数+1] J --> K[pexpire 刷新过期时间] K --> L[返回 nil] L --> M[重入成功] M --> H I -->|否,被其他线程持有| N[返回剩余 TTL] N --> O[订阅锁释放通知] O --> P[信号量等待 TTL 时间] P --> Q{超时前收到通知?} Q -->|是| B Q -->|否,超时| R[再次尝试获取] R --> B六、释放锁流程深度剖析
6.1 释放锁入口方法
Redisson 可重入锁的释放入口是RedissonLock.unlock()方法:
// RedissonLock.java @Override public void unlock() { try { get(unlockAsync(Thread.currentThread().getId())); } catch (RedisException e) { if (e.getCause() instanceof IllegalMonitorStateException) { throw (IllegalMonitorStateException) e.getCause(); } else { throw e; } } } @Override public RFuture<Void> unlockAsync(long threadId) { // 执行释放锁的 Lua 脚本 RPromise<Void> result = new RedissonPromise<Void>(); RFuture<Boolean> future = unlockInnerAsync(threadId); future.onComplete((opStatus, e) -> { // 取消看门狗续期 cancelExpirationRenewal(threadId); if (e != null) { result.tryFailure(e); return; } // 如果锁已经完全释放,发布通知 if (opStatus) { // 发布锁释放通知 } result.trySuccess(null); }); return result; }6.2 核心 Lua 脚本:释放锁逻辑
释放锁的核心 Lua 脚本:
protected RFuture<Boolean> unlockInnerAsync(long threadId) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, // Lua 脚本开始 "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + "return nil; " + "end; " + "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + "if (counter > 0) then " + "redis.call('pexpire', KEYS[1], ARGV[2]); " + "return 0; " + "else " + "redis.call('del', KEYS[1]); " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; " + "end; " + "return nil;", // Lua 脚本结束 Arrays.asList(getRawName(), getChannelName()), // KEYS[1]: 锁名称, KEYS[2]: 发布订阅频道 LockPubSub.UNLOCK_MESSAGE, // ARGV[1]: 解锁消息 internalLockLeaseTime, // ARGV[2]: 锁租约时间 getLockName(threadId) // ARGV[3]: UUID:线程ID ); }这个脚本的逻辑分为三个分支:
分支一:锁不属于当前线程(非法释放)
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end;如果 Hash 中不存在当前线程的 field,说明锁不属于当前线程,返回 nil。Java 层收到 nil 后会抛出IllegalMonitorStateException,这与 Java 的ReentrantLock行为一致。
分支二:重入次数减 1 后仍大于 0(部分释放)
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; end;将重入次数减 1。如果结果仍大于 0,说明只是释放了一层重入,锁还没有完全释放。此时刷新过期时间,返回 0。
分支三:重入次数减到 0(完全释放)
else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end;当重入次数减到 0 时,说明锁已经完全释放。执行:
del KEYS[1]:删除锁对应的 Key。publish KEYS[2] ARGV[1]:向发布订阅频道发送解锁消息,通知所有等待该锁的客户端。- 返回 1 表示锁已完全释放。
6.3 释放锁流程图
flowchart TD A[调用 unlock] --> B[执行 Lua 脚本] B --> C{当前线程是否持有锁?} C -->|否| D[返回 nil] D --> E[抛出 IllegalMonitorStateException] C -->|是| F[hincrby 重入次数-1] F --> G{重入次数 > 0?} G -->|是| H[pexpire 刷新过期时间] H --> I[返回 0] I --> J[锁部分释放,仍持有] G -->|否,重入次数=0| K[del 删除锁 Key] K --> L[publish 发布解锁通知] L --> M[返回 1] M --> N[锁完全释放] N --> O[取消看门狗续期] O --> P[通知等待线程] P --> Q[等待线程被唤醒] Q --> R[重新尝试获取锁]七、看门狗(Watchdog)机制详解
7.1 为什么需要看门狗
在分布式锁的使用中,有一个经典难题:如果锁的过期时间到了,但业务逻辑还没执行完怎么办?
举个例子:假设我们设置锁的过期时间为 10 秒,但业务逻辑因为网络延迟、GC 停顿等原因执行了 15 秒。那么在第 10 秒时,锁自动过期释放,另一个线程在第 11 秒获取了锁,导致两个线程同时操作共享资源,产生并发问题。
如果简单地把过期时间设置得很长(比如 5 分钟),虽然可以降低锁提前过期的风险,但会带来另一个问题:如果客户端崩溃,锁要 5 分钟后才能自动释放,其他线程需要等待太长时间。
Redisson 的解决方案是看门狗(Watchdog)机制:在锁没有显式指定过期时间时,默认设置 30 秒的租约,然后启动一个后台任务,每隔 10 秒(过期时间的 1/3)自动续期,将锁的过期时间重新设置为 30 秒。只要客户端还活着,锁就不会过期;一旦客户端崩溃,看门狗任务停止,锁在最多 30 秒后自动释放。
7.2 看门狗的实现原理
看门狗的核心实现位于RedissonBaseLock类中:
// RedissonBaseLock.java protected void scheduleExpirationRenewal(long threadId) { // 1. 创建续期任务 ExpirationEntry entry = new ExpirationEntry(); // 2. 放入过期续期映射表 ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent( getEntryName(), entry); if (oldEntry != null) { // 已经存在续期任务,只需增加重入计数 oldEntry.addThreadId(threadId); } else { // 第一次加锁,启动续期任务 entry.addThreadId(threadId); renewExpiration(); } } // 续期核心逻辑 private void renewExpiration() { ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName()); if (ee == null) { return; } Timeout task = commandExecutor.getConnectionManager() .newTimeout(timeout -> { // 检查当前线程是否还持有锁 if (ee.getThreadIds().isEmpty()) { return; } // 执行续期 Lua 脚本 RFuture<Boolean> future = renewExpirationAsync(ee.getFirstThreadId()); future.onComplete((res, e) -> { if (e != null) { // 续期失败,停止续期 EXPIRATION_RENEWAL_MAP.remove(getEntryName()); return; } if (res) { // 续期成功,递归调度下一次续期 renewExpiration(); } else { // 锁已被释放或不存在,停止续期 EXPIRATION_RENEWAL_MAP.remove(getEntryName()); } }); }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ee.setTimeout(task); } // 续期 Lua 脚本 protected RFuture<Boolean> renewExpirationAsync(long threadId) { return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, // 检查锁是否仍属于当前线程,如果是则刷新过期时间 "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return 1; " + "end; " + "return 0;", Collections.singletonList(getRawName()), internalLockLeaseTime, // 新的过期时间 getLockName(threadId) // 线程标识 ); }看门狗机制的关键设计:
- 定时调度:使用 Netty 的
HashedWheelTimer实现定时任务,每隔internalLockLeaseTime / 3(默认 10 秒)执行一次续期。 - 条件续期:续期前先检查锁是否仍然属于当前线程(通过
hexists判断),防止续期已经被释放的锁。 - 递归调度:每次续期成功后,递归调度下一次续期,直到锁被释放或客户端崩溃。
- 线程安全:使用
EXPIRATION_RENEWAL_MAP(ConcurrentHashMap)管理续期任务,确保同一个锁只有一个续期任务在运行。
7.3 看门狗的启动与停止
看门狗的启停时机非常关键:
- 启动时机:在
tryAcquireAsync()成功后,如果leaseTime == -1(即用户没有指定过期时间),则调用scheduleExpirationRenewal()启动看门狗。 - 停止时机:在
unlockAsync()中,无论释放是否完全成功,都会调用cancelExpirationRenewal()取消看门狗。 - 特殊情况:如果用户指定了
leaseTime(即调用lock(10, TimeUnit.SECONDS)),则不会启动看门狗,锁会在指定时间后自动过期。
7.4 看门狗时序图
sequenceDiagram participant Client as 客户端 participant Redis as Redis participant Watchdog as 看门狗 Client->>Redis: 1. 加锁 (Lua 脚本) Redis-->>Client: 加锁成功 Client->>Watchdog: 2. 启动看门狗 Watchdog->>Watchdog: 3. 等待 10 秒 loop 每 10 秒一次 Watchdog->>Redis: 4. 检查锁状态 + 续期 Redis-->>Watchdog: 续期成功,过期时间重置为 30 秒 Watchdog->>Watchdog: 5. 继续等待 10 秒 end Client->>Redis: 6. 释放锁 (Lua 脚本) Redis-->>Client: 释放成功 Client->>Watchdog: 7. 停止看门狗 Watchdog->>Watchdog: 8. 停止续期循环八、可重入锁的实战应用
8.1 基础使用示例
最简单的 Redisson 可重入锁使用方式:
// 1. 配置 Redisson 客户端 Config config = new Config(); config.useSingleServer() .setAddress("redis://127.0.0.1:6379") .setPassword("your_password") .setDatabase(0); RedissonClient redisson = Redisson.create(config); // 2. 获取可重入锁 RLock lock = redisson.getLock("order:lock:1001"); // 3. 加锁 lock.lock(); try { // 业务逻辑 System.out.println("执行业务操作"); } finally { // 4. 释放锁 lock.unlock(); }8.2 带超时的加锁示例
在实际项目中,推荐使用带超时和过期时间的加锁方式:
public class OrderService { @Autowired private RedissonClient redisson; public void deductStock(Long productId, int quantity) { String lockKey = "stock:lock:" + productId; RLock lock = redisson.getLock(lockKey); boolean locked = false; try { // 尝试加锁:最多等待 3 秒,锁 10 秒后自动释放 locked = lock.tryLock(3, 10, TimeUnit.SECONDS); if (!locked) { throw new RuntimeException("系统繁忙,请稍后重试"); } // 扣减库存的业务逻辑 doDeductStock(productId, quantity); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("获取锁被中断", e); } finally { if (locked && lock.isHeldByCurrentThread()) { lock.unlock(); } } } private void doDeductStock(Long productId, int quantity) { // 实际扣减库存逻辑 } }8.3 可重入性验证示例
验证 Redisson 锁的可重入特性:
public class ReentrantTest { private final RedissonClient redisson; private final RLock lock; public ReentrantTest(RedissonClient redisson) { this.redisson = redisson; this.lock = redisson.getLock("reentrant:test"); } public void outerMethod() { lock.lock(); try { System.out.println("进入外层方法,重入次数: " + lock.getHoldCount()); innerMethod(); System.out.println("离开外层方法,重入次数: " + lock.getHoldCount()); } finally { lock.unlock(); } } public void innerMethod() { lock.lock(); // 同一个线程再次获取同一把锁 try { System.out.println("进入内层方法,重入次数: " + lock.getHoldCount()); } finally { lock.unlock(); } } // 输出: // 进入外层方法,重入次数: 1 // 进入内层方法,重入次数: 2 // 离开外层方法,重入次数: 1 }8.4 注解方式简化使用
可以通过 AOP 自定义注解简化分布式锁的使用:
// 自定义注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RedisLock { String key(); // 锁的 Key long waitTime() default 3; // 等待时间(秒) long leaseTime() default 10; // 持有时间(秒) } // AOP 切面 @Aspect @Component public class RedisLockAspect { @Autowired private RedissonClient redisson; @Around("@annotation(redisLock)") public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable { String lockKey = parseLockKey(redisLock.key(), joinPoint); RLock lock = redisson.getLock(lockKey); boolean locked = lock.tryLock( redisLock.waitTime(), redisLock.leaseTime(), TimeUnit.SECONDS ); if (!locked) { throw new RuntimeException("获取锁失败"); } try { return joinPoint.proceed(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } private String parseLockKey(String keyExpression, ProceedingJoinPoint joinPoint) { // 使用 SpEL 解析锁 Key // 实现省略 return keyExpression; } } // 使用示例 @Service public class ProductService { @RedisLock(key = "'product:lock:' + #productId", waitTime = 5, leaseTime = 30) public void updateProduct(Long productId, ProductDTO dto) { // 业务逻辑 } }九、源码关键类深度解析
9.1 RedissonLock 核心字段
public class RedissonLock extends RedissonBaseLock { // 锁的默认租约时间,默认 30 秒 protected long internalLockLeaseTime; // 命令执行器,用于与 Redis 通信 final CommandAsyncExecutor commandExecutor; // 客户端唯一标识(UUID) final String id; // 发布订阅服务,用于锁释放通知 protected final LockPubSub pubSub; // 构造函数 public RedissonLock(CommandAsyncExecutor commandExecutor, String name) { super(commandExecutor, name); this.commandExecutor = commandExecutor; this.id = commandExecutor.getConnectionManager().getId(); this.internalLockLeaseTime = commandExecutor .getConnectionManager().getCfg() .getLockWatchdogTimeout(); // 默认 30000ms this.pubSub = commandExecutor.getConnectionManager() .getSubscribeService().getLockPubSub(); } }9.2 获取锁标识的方法
// RedissonBaseLock.java protected String getLockName(long threadId) { return id + ":" + threadId; } // 示例:如果客户端 UUID 是 "a1b2c3d4-e5f6-7890-abcd-ef1234567890" // 线程 ID 是 42 // 则返回 "a1b2c3d4-e5f6-7890-abcd-ef1234567890:42"9.3 判断锁持有者的方法
// RedissonLock.java @Override public boolean isHeldByCurrentThread() { return isHeldByThread(Thread.currentThread().getId()); } public boolean isHeldByThread(long threadId) { RFuture<Boolean> future = commandExecutor.writeAsync( getRawName(), LongCodec.INSTANCE, RedisCommands.HEXISTS, getRawName(), getLockName(threadId) ); return get(future); } @Override public boolean isLocked() { RFuture<Boolean> future = commandExecutor.writeAsync( getRawName(), LongCodec.INSTANCE, RedisCommands.EXISTS, getRawName() ); return get(future); }9.4 获取重入次数
// RedissonLock.java @Override public int getHoldCount() { RFuture<Long> future = commandExecutor.writeAsync( getRawName(), LongCodec.INSTANCE, RedisCommands.HGET, getRawName(), getLockName(Thread.currentThread().getId()) ); Long count = get(future); return count == null ? 0 : count.intValue(); }十、常见问题与最佳实践
10.1 锁的粒度问题
问题:锁的粒度太粗导致并发性能下降,粒度太细导致锁管理复杂。
最佳实践:
- 按业务维度设计锁的 Key,如
order:lock:{orderId}、stock:lock:{skuId}。 - 只锁住需要互斥操作的最小代码范围,避免在锁内执行耗时操作(如 RPC 调用、数据库查询)。
- 对于读多写少的场景,考虑使用 Redisson 的读写锁(
RReadWriteLock)。
10.2 死锁问题
问题:多个锁的获取顺序不一致导致死锁。
// 线程 A lock1.lock(); lock2.lock(); // 等待 lock2 // 线程 B lock2.lock(); lock1.lock(); // 等待 lock1,死锁!最佳实践:
- 所有业务代码中获取多个锁的顺序保持一致。
- 使用
tryLock(waitTime, leaseTime, unit)设置超时时间,避免无限等待。 - 使用 Redisson 的联锁(
RMultiLock),一次性获取多个锁,避免死锁。
10.3 锁释放的安全性
问题:在 finally 块中释放锁时,如果锁已经过期或被其他线程释放,会抛出异常。
最佳实践:在释放锁前检查当前线程是否仍持有锁:
finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }10.4 主从架构下的锁安全问题
问题:在主从复制架构中,如果主节点在写入锁信息后宕机,从节点提升为主节点时可能丢失锁信息,导致两个客户端同时持有锁。
最佳实践:
- 对于高安全性要求的场景,使用 Redisson 的红锁(
RedLock),该算法需要至少 3 个独立的 Redis 主节点。 - 对于一般场景,可以接受主从切换时的短暂不一致,配合业务层面的幂等性设计来保证最终一致性。
10.5 性能优化建议
- 连接池配置:合理配置 Redisson 的连接池大小,避免连接数不足导致超时。
- 序列化方式:Redisson 锁的存储使用
LongCodec,不需要额外的序列化开销。 - 避免热点 Key:如果某个锁的竞争非常激烈,考虑将锁的粒度进一步细化,或者使用分段锁(类似
ConcurrentHashMap的思想)。 - 合理使用 leaseTime:如果业务逻辑执行时间可以预估,显式指定
leaseTime可以避免看门狗的开销。
十一、总结与展望
本文作为 Redisson 分布式锁系列的第一篇,深入剖析了可重入同步锁的架构设计和核心实现原理。我们从分布式锁的基本概念出发,逐步深入到 Redisson 的架构设计、加锁与释放锁的 Lua 脚本、看门狗机制以及实战应用。
Redisson 可重入锁的核心设计亮点包括:
- Hash 数据结构:使用 Redis Hash 存储锁信息,通过 field 区分不同客户端和线程,通过 value 记录重入次数。
- Lua 脚本原子性:所有加锁和释放锁操作都通过 Lua 脚本在 Redis 中原子执行,保证并发安全。
- 看门狗机制:通过后台定时任务自动续期,解决锁提前过期的问题,同时保证客户端崩溃后锁能自动释放。
- 发布订阅通知:通过 Redis PubSub 实现锁释放通知,避免轮询带来的性能开销。
- 可重入设计:借鉴 Java
ReentrantLock的思想,支持同一个线程多次获取同一把锁。
在下一篇文章中,我们将继续深入 Redisson 的其他锁类型,包括公平锁(FairLock)、联锁(MultiLock)和红锁(RedLock),探讨它们在分布式环境下的应用场景和实现原理。
理解 Redisson 可重入锁的架构设计,不仅有助于我们在项目中正确使用分布式锁,更能让我们深入理解分布式系统设计中的一致性、高可用和性能权衡等核心思想。