Redisson分布式锁核心实现解析

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)通过hexistshincrby支持可重入锁;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() → [TimerTaskrenewExpirationAsync递归]

初始化任务

当线程首次获取锁时会创建续期任务并立即执行,后续同一线程重入时只需记录线程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); // 清理任务
        }
    }
});
相关推荐
拉不动的猪6 分钟前
Vue 3 中 async setup () 的「坑」与避坑指南1
前端·javascript·面试
程序猿阿越8 分钟前
Kafka源码(二)分区新增和重分配
java·后端·源码阅读
拉不动的猪8 分钟前
Vue 3 中 async setup () 的「坑」与避坑指南2
前端·vue.js·后端
lizhongxuan19 分钟前
AI代理的上下文工程:构建Manus的经验教训
后端
格格步入22 分钟前
🫵记一次协助排查问题(COLA架构)
后端·领域驱动设计
_一条咸鱼_24 分钟前
Android Runtime调试检测与反制手段(86)
android·面试·android jetpack
五号厂房24 分钟前
一道关于事件循环(Event Loop) 机制和任务队列模型的面试题
前端·面试
爱分享的程序员37 分钟前
前端面试专栏-工程化:27.工程化实践(CI/CD、代码规范)
前端·ci/cd·面试
心之语歌1 小时前
Spring AI 聊天记忆
java·后端
胡gh1 小时前
一篇文章,带你搞懂大厂如何考察你对Array的理解
javascript·后端·面试