一、类结构与设计思想

1. 继承关系
java
public class RedissonLock extends RedissonBaseLock
RedissonBaseLock:提供锁的基础能力(续期、解锁通知)
RedissonLock:实现具体锁逻辑(可重入、非公平)
2. 核心成员变量
java
protected long internalLockLeaseTime; // 锁租约时间(默认30秒)
protected final LockPubSub pubSub; // 锁的发布订阅管理器
final CommandAsyncExecutor commandExecutor; // Redis命令执行器
二、核心方法深度解析
1. 加锁入口:lock()
java
public void lock() {
try {
lock(-1, null, false); // 无限等待,不可中断
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
-1:表示无限等待
null:使用默认租约时间(Watchdog自动续期)
false:不支持中断
2. 核心加锁流程:lock(long leaseTime, TimeUnit unit, boolean interruptibly)
这是整个锁的核心逻辑:
关键代码解析:
java
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
/**
* 获取当前线程ID,用于可重入锁识别
* 不是用Thread.currentThread()对象,因为对象可能被GC,但ID是唯一的
* 实战注意:线程池中线程可能被复用,但ID不变,仍能正确识别重入
*/
long threadId = Thread.currentThread().getId();
/**
* -1:waitTime,表示立即返回,不等待
* leaseTime:锁持有时间(null表示使用Watchdog自动续期)
* unit:时间单位
* threadId:用于可重入判断
*/
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// 第一次尝试获取锁
//为什么先try一次?快速路径(Fast Path)优化:大多数情况下锁是可用的,一次尝试就能成功,避免订阅开销。
/**
* null:获取成功(Lua脚本返回nil)
* 正数:锁被占用,返回剩余生存时间(毫秒)
* 负数:理论上不会出现(Redisson总是设置过期时间)
* 性能优化:这里直接返回,没有后续操作,热路径(Hot Path)最短化。
*/
if (ttl == null) { //获取成功
return;
}
// 获取失败,订阅锁释放消息
//订阅什么?
/**
* 订阅Redis频道:redisson_lock__channel:{lock_name}
* 当锁释放时,持有者会向这个频道发布UNLOCK_MESSAGE
* 为什么用CompletableFuture?
* 异步订阅,避免阻塞当前线程。但后面会同步等待订阅完成。
*/
CompletableFuture<RedissonLockEntry> future = subscribe(threadId);//异步订阅,避免阻塞当前线程。但后面会同步等待订阅完成。
pubSub.timeout(future);//超时保护
RedissonLockEntry entry;
//防止网络分区时客户端无限等待订阅。
/**
* 两种等待策略:
* getInterrupted():可中断等待,响应Thread.interrupt()
* get():不可中断等待,忽略中断
* 设计哲学:
* lock():不可中断,确保锁语义完整性
* lockInterruptibly():可中断,适合需要取消的长时间操作
*/
if (interruptibly) {
entry = commandExecutor.getInterrupted(future);
} else {
entry = commandExecutor.get(future);
}
try {
while (true) {
// 循环尝试获取锁
/**
* 为什么在循环内再次tryAcquire?
* 双重检查(Double-Check):
* 第一次检查后到订阅完成前,锁可能已被释放
* 避免收到解锁消息后还要再等一次网络RTT
*/
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {// 获取成功
break;
}
// 等待锁释放信号
//情况1:知道锁何时过期(ttl >= 0)
/**
* 为什么用tryAcquire(ttl)而不是acquire()?
* 精确超时:最多等待锁的剩余生存时间,避免:
* 锁永远不会释放(客户端崩溃但未设置过期时间)
* 无意义的长等待
* 中断处理策略:
* 可中断模式:传播中断异常,允许业务响应取消
* 不可中断模式:忽略中断,继续等待,保证锁获取的原子性
*/
if (ttl >= 0) {
try {
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);//信号量机制
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);//信号量机制
}
} else {
//情况2/3:不知道锁何时过期(ttl < 0)
//理论上ttl < 0不会出现(Redisson总是设置过期时间)
//这是防御性编程,防止极端情况
//acquire() vs acquireUninterruptibly():是否响应中断
if (interruptibly) {
entry.getLatch().acquire();
} else {
entry.getLatch().acquireUninterruptibly();
}
}
}
} finally {
/**
* 为什么在finally中?
* 资源泄漏防护:
* 获取锁成功:需要取消订阅
* 获取锁失败(超时):需要取消订阅
* 线程被中断:需要取消订阅
* 发生异常:需要取消订阅
*/
/**
* 从订阅列表移除当前线程
* 如果没有其他等待者,取消Redis频道订阅
* 释放信号量资源
*/
unsubscribe(entry, threadId);// 清理订阅
}
// get(lockAsync(leaseTime, unit));
}
3. 尝试获取锁的核心:tryLockInnerAsync()

lua
Lua脚本解析:
-- KEYS[1]: 锁的key(如"myLock")
-- ARGV[1]: 锁的过期时间(毫秒)
-- ARGV[2]: 锁的唯一标识(UUID + threadId)
-- 情况1:锁不存在 或 当前线程已持有锁(可重入)
java
if (redis.call('exists', KEYS[1]) == 0) or
(redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 可重入锁:计数器+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
-- 设置过期时间(续期)
redis.call('pexpire', KEYS[1], ARGV[1]);
-- 返回nil表示获取锁成功
return nil;
end;
-- 情况2:锁被其他线程持有
-- 返回锁的剩余生存时间(毫秒)
return redis.call('pttl', KEYS[1]);
4. Watchdog自动续期:scheduleExpirationRenewal()
在分布式系统中,为锁设置超时时间是必要的安全措施,防止客户端崩溃导致死锁。但这就带来了两难选择:
超时太短:业务未执行完锁就过期,其他线程获取锁导致数据不一致
超时太长:客户端崩溃后锁长时间不释放,系统可用性下降
Redisson的Watchdog机制通过自动续期+心跳保活完美解决了这个困境:
保活机制:业务执行期间自动续期,锁永不过期
安全兜底:客户端崩溃后,锁会在租约到期后自动释放
智能调度:按需启动,共享续期,最小化系统开销
(1). 全局续期映射表:EXPIRATION_RENEWAL_MAP
java
/**
* 锁名 → 续期条目的全局映射表
*
* 设计特点:
* 1. ConcurrentHashMap保证线程安全
* 2. 锁粒度:每个锁名对应一个Entry
* 3. 生命周期:从第一个线程获取锁到最后一个线程释放锁
*/
private static final ConcurrentMap<String, ExpirationEntry>
EXPIRATION_RENEWAL_MAP = new ConcurrentHashMap<>();
(2). 续期条目:ExpirationEntry
java
/**
* 锁续期管理单元,核心职责:
*
* 1. 线程管理:记录所有持有该锁的线程及其重入次数
* 2. 任务控制:持有Watchdog定时任务的引用
* 3. 引用计数:支持可重入锁的正确续期
*/
public class ExpirationEntry {
// LinkedHashMap保持插入顺序,保证公平性
private final Map<Long, Integer> threadIds = new LinkedHashMap<>();
// Timeout引用,用于取消定时任务
private volatile Timeout timeout;
// 线程安全的引用计数管理
public void addThreadId(long threadId) {
threadIds.merge(threadId, 1, Integer::sum);
}
}
(3).scheduleExpirationRenewal方法:启动续期的入口
java
/**
* 启动锁的自动续期守护线程(Watchdog)
*
* 设计目标:
* 1. 并发安全:多个线程同时获取同一把锁时正确工作
* 2. 资源共享:所有持有同一把锁的线程共享一个Watchdog
* 3. 懒加载:只有需要时才启动续期任务
*
* 触发条件:当锁获取成功且没有指定明确的租约时间时
*/
protected void scheduleExpirationRenewal(long threadId) {
// 创建新的续期条目(可能被丢弃)
ExpirationEntry entry = new ExpirationEntry();
// 原子操作:锁名→条目的映射
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(
getEntryName(), // 锁的唯一标识
entry // 新建的条目
);
if (oldEntry != null) {
// 情况1:锁已被其他线程持有
// 只是增加当前线程的引用计数,共享已有的Watchdog
oldEntry.addThreadId(threadId);
} else {
// 情况2:当前线程是第一个获取锁的
// 添加线程ID,并启动Watchdog守护线程
entry.addThreadId(threadId);
renewExpiration(); // 启动续期循环
}
}
(4).renewExpiration方法:递归续期循环
java
/**
* 创建并调度下一次续期任务
*
* 核心设计:递归而非循环
* 优势:
* 1. 异常安全:某次续期失败不会影响后续调度
* 2. 动态调整:每次都可以重新计算执行时间
* 3. 资源节约:没有任务时立即停止递归
*/
private void renewExpiration() {
// 双重检查:锁可能已被释放
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return; // 锁已释放,无需续期
}
// 创建延迟任务,internalLockLeaseTime/3后执行
// 默认30秒租约 → 每10秒续期一次
Timeout task = commandExecutor.getConnectionManager()
.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) {
performExpirationRenewal();
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
// 保存任务引用,便于后续取消
ee.setTimeout(task);
}
(5).performExpirationRenewal:续期执行逻辑
java
private void performExpirationRenewal() {
// 第一层:内存状态校验
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return; // 锁条目已被移除,停止续期
}
// 第二层:线程持有校验
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return; // 没有持有线程,停止续期
}
// 第三层:Redis持有权验证
RFuture<Boolean> future = renewExpirationAsync(threadId);
// 异步回调处理续期结果
future.whenComplete((res, e) -> {
if (e != null) {
// 网络或Redis异常,停止续期
log.error("Can't update lock expiration", e);
return;
}
if (res) {
// 续期成功,递归调用继续下一次
renewExpiration();
}
// res==false表示锁已不属于当前客户端,停止续期
});
}
续期Lua脚本(renewExpirationAsync):
java
-- renewExpirationAsync的核心脚本
-- 参数:KEYS[1]=锁key, ARGV[1]=30000(30秒), ARGV[2]=客户端标识
-- 关键验证:锁是否仍由当前客户端持有
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 验证通过,续期30秒
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1; -- 续期成功
end;
-- 验证失败,锁已易主或不存在
return 0; -- 续期失败
为什么需要Lua脚本的原子验证?
防止竞态条件:在检查锁状态和执行续期之间,锁可能已被其他客户端获取。
(6).续期任务的取消机制
java
/**
* 取消锁的自动续期
*
* 触发时机:
* 1. 锁被正常释放(unlock)
* 2. 锁被强制释放(forceUnlock)
* 3. 客户端关闭(shutdown)
*
* 引用计数管理:
* - 每个线程释放锁时减少计数
* - 只有所有线程都释放锁时才取消Watchdog
*/
protected void cancelExpirationRenewal(Long threadId) {
ExpirationEntry entry = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (entry == null) {
return;
}
if (threadId != null) {
// 减少特定线程的引用计数
Integer count = entry.threadIds.get(threadId);
if (count != null) {
if (count > 1) {
// 还有重入,只减少计数
entry.threadIds.put(threadId, count - 1);
return;
}
// 计数归零,移除线程
entry.threadIds.remove(threadId);
}
}
// 所有线程都释放了锁
if (threadId == null || entry.threadIds.isEmpty()) {
// 取消定时任务
if (entry.timeout != null) {
entry.timeout.cancel();
}
// 从全局映射中移除
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
}
5. 解锁核心:unlockInnerAsync()
java
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteSyncedNoRetryAsync(getRawName(), LongCodec.INSTANCE,
RedisCommands.EVAL_BOOLEAN,
"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 " + // 可重入次数>0,不删除锁
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " + // 可重入次数=0,删除锁
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " + // 发布解锁消息
"return 1; " +
"end; ",
Arrays.asList(getRawName(), getChannelName()),
LockPubSub.UNLOCK_MESSAGE,
internalLockLeaseTime,
getLockName(threadId));
}
6. 发布订阅机制:LockPubSub
java
/**
* 分布式锁的发布订阅处理器
*
* 设计意图:
* 1. 避免传统的轮询(polling)机制,减少Redis压力
* 2. 实现事件驱动的锁等待,提高性能
* 3. 支持多个客户端同时等待同一把锁
*
* 工作原理:
* 当线程尝试获取锁失败时,会订阅Redis频道:redisson_lock__channel:{lock_name}
* 锁持有者释放锁时,会向该频道发布UNLOCK_MESSAGE消息
* 所有订阅者收到消息后,竞争获取锁(避免惊群效应)
*/
public class LockPubSub extends PublishSubscribe<RedissonLockEntry> {
// 普通锁释放消息标识(写锁)
public static final Long UNLOCK_MESSAGE = 0L;
// 读锁释放消息标识(用于读写锁场景)
public static final Long READ_UNLOCK_MESSAGE = 1L;
/**
* 创建新的锁等待条目
*
* 关键设计:
* 每个锁对应一个RedissonLockEntry,所有等待该锁的线程共享这个entry
* entry内部维护信号量(Semaphore)和监听器队列
*
* @param oldEntry 旧的entry(用于连接重连时的状态恢复)
* @return 新的RedissonLockEntry实例
*/
@Override
protected RedissonLockEntry createEntry(PubSubEntry<RedissonLockEntry> oldEntry) {
return new RedissonLockEntry();
}
/**
* 收到Redis频道消息时的回调处理
*
* 核心算法:
* 1. 从监听器队列中取出一个待执行任务(FIFO顺序)
* 2. 执行该任务(通常是再次尝试获取锁)
* 3. 释放信号量的一个许可,唤醒一个等待线程
*
* 为什么只唤醒一个线程?
* - 避免"惊群效应":多个线程同时被唤醒,但只有一个能获取锁
* - 减少无效竞争:其他线程继续等待,避免CPU和Redis的无效压力
*
* @param value 当前锁对应的RedissonLockEntry
* @param message 收到的消息(UNLOCK_MESSAGE或READ_UNLOCK_MESSAGE)
*/
@Override
protected void onMessage(RedissonLockEntry value, Long message) {
if (message.equals(UNLOCK_MESSAGE)) {
// 先执行监听器任务(通常是立即尝试获取锁)
Runnable runnable = value.getListeners().poll();
if (runnable != null) {
runnable.run(); // 执行获取锁的尝试
}
// 释放信号量,唤醒一个等待线程
value.getLatch().release(); // release(1),只释放一个许可
}
}
}
/**
* 锁等待条目的核心实现类
*
* 双重职责:
* 1. 管理等待线程的同步(通过信号量)
* 2. 管理异步回调任务(通过监听器队列)
*
* 数据结构设计:
* - Semaphore latch:控制线程挂起/唤醒
* - ConcurrentLinkedQueue<Runnable> listeners:存储待执行的异步任务
*
* 并发控制:
* 使用线程安全的数据结构,支持多线程并发访问
* 同一把锁的所有等待线程共享同一个entry实例
*/
public class RedissonLockEntry implements PubSubEntry<RedissonLockEntry> {
/**
* 信号量(许可数为0)
*
* 设计原理:
* - 初始许可数为0:所有尝试acquire的线程都会阻塞
* - 收到解锁消息时release(1):释放一个许可,唤醒一个线程
* - 实现了"精准唤醒"而非"广播唤醒"
*
* 为什么用Semaphore而不是Object.wait()?
* 1. 支持超时:tryAcquire(timeout, unit)
* 2. 支持中断:acquire()可响应中断
* 3. 许可控制:可精确控制唤醒的线程数量
* 4. 无需同步块:减少死锁风险
*/
private final Semaphore latch = new Semaphore(0);
/**
* 监听器队列(先进先出)
*
* 作用:
* 1. 存储异步回调任务,通常是立即尝试获取锁的逻辑
* 2. 保证公平性:先订阅的线程优先被唤醒(近似公平)
* 3. 异步任务与线程唤醒解耦:先执行任务再唤醒线程
*
* 为什么用ConcurrentLinkedQueue?
* 1. 无界队列:避免任务丢失
* 2. 线程安全:支持多生产者(等待线程)单消费者(onMessage线程)
* 3. 高性能:无锁算法,CAS操作
* 4. ConcurrentLinkedQueue保证FIFO顺序
*/
private final ConcurrentLinkedQueue<Runnable> listeners = new ConcurrentLinkedQueue<>();
/**
* 阻塞获取许可(无限等待)
*
* 使用场景:
* 当锁没有设置过期时间(理论上不会出现)或采用不可中断模式时
*
* 注意:此方法会阻塞当前线程,直到:
* 1. 收到解锁消息(release()被调用)
* 2. 线程被中断(如果使用acquireInterruptibly)
*/
public void acquire() {
latch.acquire();
}
/**
* 尝试获取许可(带超时)
*
* 核心用途:
* 当锁设置了过期时间时,最多等待锁的剩余生存时间
* 避免锁永远不释放时线程无限等待
*
* @param timeout 最大等待时间
* @param unit 时间单位
* @return true-获取成功,false-超时
*/
public boolean tryAcquire(long timeout, TimeUnit unit) {
return latch.tryAcquire(timeout, unit);
}
/**
* 释放一个许可,唤醒一个等待线程
*
* 调用时机:
* 1. LockPubSub.onMessage()收到解锁消息时
* 2. 锁等待超时取消时(清理资源)
*
* 注意:只释放一个许可,避免惊群效应
*/
public void release() {
latch.release(); // 等价于release(1)
}
}
整个机制的工作原理图

"Redisson通过发布订阅模式+信号量控制实现了高效的锁等待机制,核心设计有三层:
第一层:事件驱动代替轮询
线程获取锁失败后,不是轮询Redis,而是订阅特定频道。锁释放时通过Redis Pub/Sub通知,实现了零轮询、低延迟的等待。
第二层:精准唤醒避免惊群
每个锁对应一个RedissonLockEntry,内部用Semaphore控制线程挂起。解锁时只release(1),唤醒一个线程,避免多个线程同时竞争造成的资源浪费。
第三层:异步任务与同步等待解耦
通过ConcurrentLinkedQueue管理异步回调任务,收到解锁消息时先执行任务(尝试获取锁),再唤醒线程。这种任务先行的设计提高了响应速度。
关键优化点:
1、调整subscriptionConnectionPoolSize应对高并发
2、监控等待队列长度,预防死锁
3、合理设置超时时间,平衡响应速度和资源占用"
三、关键设计模式
1. 模板方法模式
RedissonBaseLock:定义骨架(续期、解锁)
RedissonLock:实现具体逻辑
2. 命令模式
CommandAsyncExecutor commandExecutor;
所有Redis操作都封装成Command对象,支持同步/异步执行。
3. 观察者模式
entry.addListener(listener); // 添加监听器
entry.removeListener(listener); // 移除监听器
等待锁的线程注册监听器,收到解锁消息后被唤醒。
4. 工厂模式
RedissonClient client = Redisson.create(config);
RLock lock = client.getLock("myLock");
通过工厂方法创建各种分布式对象。
四、源码中的精妙设计
1. 双重检查避免重复订阅
java
// 在lockAsync方法中
CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(currentThreadId);
pubSub.timeout(subscribeFuture); // 设置超时
2. 超时控制链式传递
java
public void timeout(CompletableFuture<RedissonLockEntry> future, long timeout) {
if (future.isDone()) {
return;
}
// 设置超时,避免无限等待
getServiceManager().newTimeout(t -> {
if (!future.isDone()) {
future.completeExceptionally(new TimeoutException());
}
}, timeout, TimeUnit.MILLISECONDS);
}
3. 内存泄漏防护
java
@Override
protected void cancelExpirationRenewal(Long threadId) {
ExpirationEntry entry = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (entry == null || !entry.hasThreadId(threadId)) {
return;
}
// 清理Watchdog线程
Timeout task = entry.getTimeout();
if (task != null) {
task.cancel();
}
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
4. 异步回调链
java
// 典型的异步编程模式
ttlFuture.whenComplete((ttl, e) -> {
if (e != null) {
result.completeExceptionally(e);
return;
}
if (ttl == null) {
result.complete(null);
} else {
// 继续下一步
}
});
总结:
1、 整体架构
RedissonLock的核心设计围绕三个机制:1)基于Lua脚本的原子操作;2)Watchdog自动续期;3)Pub/Sub等待通知。源码中lock()方法体现了'尝试-订阅-重试'的循环模式。
2、关键方法
tryLockInnerAsync()的Lua脚本实现了可重入锁的核心逻辑,使用Redis的hash结构存储线程标识和重入次数。scheduleExpirationRenewal()创建了守护线程自动续期,解决了业务执行时间不确定的问题。
3、设计模式
代码中运用了模板方法(BaseLock定义骨架)、观察者(锁释放通知)、命令(Redis操作封装)等多种设计模式,体现了良好的架构设计。
4、细节亮点
1)使用Semaphore作为等待队列,避免忙等待;
2)超时控制链式传递,防止内存泄漏;
3)异步回调的异常传播机制保证了可靠性。
5、实战启示
从源码中学到:分布式锁要考虑网络分区、时钟跳跃、客户端崩溃等异常情况。Redisson通过Watchdog、唯一标识、原子Lua脚本等机制提供了生产级的可靠性。
END
RedissonLock的核心是一个原子Lua脚本实现的可重入锁,配合Watchdog守护线程自动续期,通过Pub/Sub机制实现高效等待。这种设计既保证了正确性,又兼顾了性能,是我们项目分布式锁的首选方案。