Redisson分布式锁核心实现解析
在分布式系统中,协调多个节点对共享资源的访问是一个常见且具有挑战性的问题。分布式锁作为一种基础同步机制,能够确保在分布式环境下,同一时刻只有一个客户端可以访问临界资源,从而避免数据竞争和不一致的问题。
Redis作为高性能的内存数据库,凭借其单线程模型和原子性操作,成为实现分布式锁的理想选择。然而,直接基于Redis实现一个健壮、可靠的分布式锁并非易事,开发者需要处理锁超时、自动续约、可重入性、故障恢复等诸多复杂问题。
Redisson作为Redis的Java客户端,不仅提供了丰富的分布式对象和服务,还内置了生产级的分布式锁实现。它封装了复杂的底层逻辑,通过精心设计的机制(如看门狗自动续期、Lua脚本原子操作等)解决了原生Redis分布式锁的诸多痛点,让开发者能够以简洁的API获得高可靠的分布式锁功能。
本文将深入剖析Redisson分布式锁的源码实现,从加锁、续约到解锁的全流程,揭示其背后的设计思想与关键技术点,帮助开发者更好地理解和使用这一强大的工具。
2.1 方法签名与基础逻辑
Redisson的tryLock
方法通过支持可配置的等待时间(waitTime)和租约时间(leaseTime),结合线程ID实现可重入性识别。
java
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)
throws InterruptedException {
// 时间转换与初始化
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
// 尝试获取锁
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
if (ttl == null) { // 锁获取成功
return true;
}
...
}
2.2 锁订阅与等待机制
锁订阅与等待机制:通过Redis的pub/sub监听锁释放事件避免轮询消耗,结合Semaphore实现线程精准挂起/唤醒。
java
// 订阅锁释放事件
CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// 带超时的等待订阅完成
subscribeFuture.get(time, TimeUnit.MILLISECONDS);
// 锁等待逻辑
do {
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
if (ttl == null) return true; // 成功获取
if (ttl >= 0L && ttl < time) {
lockEntry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
lockEntry.getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
} while(time > 0L);
信号量控制实现 :在Redisson的分布式锁等待机制中,通过RedissonLockEntry
内部的Semaphore
(信号量)实现线程的精准挂起与唤醒。当线程尝试获取锁失败时,会通过entry.getLatch().tryAcquire()
挂起当前线程,此时信号量许可数为0导致线程阻塞;而当其他线程释放锁时,Redis的pub/sub通知会触发LockPubSub
回调,通过Semaphore.release()
释放信号量,立即唤醒等待队列中的线程重新尝试获取锁。这种设计既避免了轮询带来的CPU浪费,又能实现毫秒级精度的线程调度,是Redisson高性能锁等待的核心实现之一。
java
public class RedissonLockEntry {
private final Semaphore latch = new Semaphore(0);
//...
}
2.3 原子化锁获取
方法调用链解析:
java
tryAcquire() → tryAcquireAsync0() → tryAcquireAsync() → tryLockInnerAsync()
↘ tryAcquireOnceAsync() → tryLockInnerAsync()
Lua脚本解析:通过单次Redis调用实现了「检查锁状态→记录重入次数→刷新过期时间」的完整流程。 其优势在于:1)利用Redis单线程特性保证原子性,避免并发问题;2)通过hexists
和hincrby
支持可重入锁;3)仅当线程持有锁时才续期,避免无效操作;
lua
-- 条件1:锁不存在 或 当前线程已持有锁(可重入)
if ((redis.call('exists', KEYS[1]) == 0) or
(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]);
2.4 看门狗机制
调用顺序:
scheduleExpirationRenewal()
→ renewExpiration()
→ [TimerTask
→renewExpirationAsync
→递归
]
初始化任务
当线程首次获取锁时会创建续期任务并立即执行,后续同一线程重入时只需记录线程ID,通过ConcurrentHashMap
确保每把锁只有一个续期任务,同时在finally块中处理线程中断情况防止资源泄漏,完整支持了锁的可重入特性和安全续期机制。
java
protected void scheduleExpirationRenewal(long threadId) {
// 创建新的续期记录对象(包含线程ID集合和定时任务引用)
ExpirationEntry entry = new ExpirationEntry();
// 原子性地注册到全局续期任务MAP(Key=锁名称,Value=续期记录)
// putIfAbsent保证同一把锁只有一个续期任务
ExpirationEntry oldEntry = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(
this.getEntryName(),
entry
);
if (oldEntry != null) {
// 已有续期任务:只需将当前线程ID加入集合(支持锁重入)
oldEntry.addThreadId(threadId);
} else {
// 首次创建任务:注册线程ID并启动续期流程
entry.addThreadId(threadId);
try {
// 核心操作:开始周期性续期
this.renewExpiration();
} finally {
// 如果线程被中断,立即清理任务
if (Thread.currentThread().isInterrupted()) {
this.cancelExpirationRenewal(threadId);
}
}
}
}
实现自动续期
整体结构
java
private void renewExpiration() {
// 1. 获取当前锁的续期记录
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee != null) { // 2. 如果续期任务存在
// 3. 创建定时任务(核心逻辑)
Timeout task = newTimeout(timerTask, internalLockLeaseTime/3, TimeUnit.MILLISECONDS);
ee.setTimeout(task); // 4. 绑定任务到续期记录
}
}
定时任务创建 :通过 getServiceManager().newTimeout()
创建定时续期任务,底层采用 HashedWheelTimer 时间轮算法实现高效调度:由单个后台线程统一检测并触发到期任务,既避免了为每个锁创建独立线程的资源消耗。
java
Timeout task = getServiceManager().newTimeout(
new TimerTask() {
@Override
public void run(Timeout timeout) {
// 续期逻辑实现...
}
},
internalLockLeaseTime / 3L, // 默认10秒(30/3)
TimeUnit.MILLISECONDS
);
异步续期流程 :通过递归调用+事件回调 实现,当看门狗触发续期时,首先异步向Redis发送续期请求而不阻塞线程,待收到响应后通过CompletableFuture.whenComplete()
回调处理结果------若续期成功则重新发起延时任务(形成递归调用链),失败则立即终止任务。
优势:每次续期成功后,才会安排下一次续期(不会盲目重复),而且整个过程都是异步处理(不卡线程)。这样既省资源(不浪费CPU),又能支持大量锁同时续期,效率很高。
java
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
// 异常处理:移除续期记录
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
} else {
if (res) { // 续期成功
renewExpiration(); // 递归调用实现周期续期
} else { // 续期失败(锁已释放)
cancelExpirationRenewal(null); // 清理任务
}
}
});