Redisson 分布式锁底层原理实现详解

1、概述

Redisson 是基于 Redis 实现的分布式锁,其核心思想是利用 Redis 的 SET NX(SET if Not eXists)+ PX(过期时间) 来实现锁的互斥性,同时通过 Lua 脚本 处理加锁、解锁、续期等原子操作,保证分布式环境下的安全性。

2、主要过程

2.1、加锁(tryLock)

定义: 通过 SET NX 实现锁的互斥功能。

  • 命令: SET key value NX PX expireTime
  • 作用: 使用 SETNX 保证互斥性,使用 PX 设置过期时间防止死锁
  • 数据结构:
    • key: lock:{name},如 lock:order:123
    • value: 存储唯一标识(UUID + 线程ID),用于区分不同客户端的锁
  • 特点:
    如果 key 不存在,创建并返回 OK,表示加锁成功。
    如果 key 存在,返回 nil,表示加锁失败。

在这里插入图片描述

2.2、锁续约(Watchdog 机制)

在这里插入图片描述

定义:在tryLock未设置 leaseTime 时候会启动看门狗机制,定期(默认 10s)延长30s锁的过期时间。
实现方式:
Redisson 在获取RFuture时候,如果获取锁成功,就会执行scheduleExpirationRenewal()方法,执行对应的代码

在这里插入图片描述
scheduleExpirationRenewal代码如下:

// 续期锁的过期时间
private void renewExpiration() {
    // 从过期续期映射中获取锁的过期条目
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return; // 如果找不到条目,说明没有锁需要续期
    }

    // 创建一个定时任务,用于定期续期锁的过期时间
    Timeout task = commandExecutor.getServiceManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            // 重新获取过期条目
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return; // 如果条目已被移除,结束任务
            }
            // 获取持有锁的线程 ID
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return; // 如果没有线程 ID,说明没有线程持有该锁,结束任务
            }

            // 异步续期锁的过期时间
            CompletionStage<Boolean> future = renewExpirationAsync(threadId);
            future.whenComplete((res, e) -> {
                if (e != null) {
                    // 如果续期过程中发生错误,记录日志并移除续期条目
                    log.error("Can't update lock {} expiration", getRawName(), e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }

                if (res) {
                    // 如果续期成功,重新调度续期任务
                    renewExpiration();
                } else {
                    // 如果续期失败,取消续期操作
                    cancelExpirationRenewal(null);
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 定时任务每 internalLockLeaseTime / 3 毫秒执行一次
    
    // 设置定时任务到过期条目中
    ee.setTimeout(task);
}

// 启动续期操作,首次获取锁时会调用此方法
protected void scheduleExpirationRenewal(long threadId) {
    // 创建新的过期条目
    ExpirationEntry entry = new ExpirationEntry();
    // 尝试将新的条目加入到续期映射中
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        // 如果条目已存在,说明已有其他线程在续期,添加当前线程 ID 到条目中
        oldEntry.addThreadId(threadId);
    } else {
        // 如果是首次添加,开始进行续期操作
        entry.addThreadId(threadId);
        try {
            // 启动锁过期续期任务
            renewExpiration();
        } finally {
            // 如果当前线程被中断,取消续期操作
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}

2.3、解锁(unlock)

命令: 通过Lua 脚本确保原子性

2.4、可重入性

实现方式: Redisson 维护一个 可重入计数器**(Hash 结构)**,如果同一线程再次获取锁,计数器递增。
存储结构:
key: lock:{name}
value: UUID:threadId 作为唯一标识
hash 结构(可重入计数器)

HINCRBY lock:{name} UUID:threadId 1

解锁时,只有计数器 == 0,才真正删除 Redis 锁

2.5、失败重试

策略:
线程获取锁失败时,不会立即放弃,而是进入 自旋等待。

主要机制:
在这里插入图片描述

通过计算计算减去锁消耗时间得到的锁最大等待时间,如果 >0 执行对应的订阅功能,订阅其他获取锁的信号,如果其他线程释放锁会发布信号,收到订阅后再执行重试获取锁机制,,等待订阅最大时间是 waitime ,从而不是直接进行循环,导致性能的损耗,最后进行 while(true) 直至获取锁成功为止。

<think>我们正在讨论Redisson的锁续期机制,特别是它如何检测持有锁的线程是否已经结束或崩溃。 根据引用[^1],我们知道Redisson通过看门狗(Watchdog)机制来实现锁的续期。这个机制会定期检查持有锁的线程是否还活跃,如果活跃则延长锁的持有时间(即续期),如果线程已经结束或崩溃,则锁会被释放。 具体来说,Redisson的锁续期机制是通过一个后台线程(看门狗线程)来实现的。当客户端成功获取锁后,会启动一个看门狗线程,该线程会定期(默认是锁过期时间的1/3)去检查锁是否还被当前线程持有。如果锁仍然被当前线程持有,那么看门狗线程就会对锁进行续期(即重新设置锁的过期时间)。 那么,如何检测线程是否结束或崩溃呢? 在Redisson中,当获取锁时,会记录当前线程的ID(在分布式环境中,这个ID通常由客户端生成,确保唯一性)。看门狗线程在续期时,会检查当前线程是否还存活(通过Java的Thread类提供的方法,如isAlive()方法)。如果线程已经终止(即不再存活),那么看门狗线程就不会再续期,从而让锁自然过期。 但是,这里有一个关键点:在分布式系统中,一个线程的崩溃(比如整个JVM崩溃)是无法通过另一个JVM(或另一个节点)来检测该线程是否存活的。因此,Redisson实际上并不是直接检测线程是否存活,而是通过以下方式: 1. 在同一个JVM内:如果持有锁的线程已经结束(正常结束或异常崩溃),那么看门狗线程(如果还在运行)会检测到当前线程已经不再存活,从而停止续期。但是,如果整个JVM崩溃,那么看门狗线程也会随之停止,因此不会进行续期操作。 2. 在分布式环境中:Redisson的锁续期机制依赖于与Redis的通信。看门狗线程会定期向Redis发送续期命令(通过Lua脚本)。如果持有锁的客户端崩溃了(比如整个JVM崩溃),那么看门狗线程也会停止,因此不会发送续期命令,锁在Redis中就会自动过期。 因此,看门狗机制的关键在于:它只在持有锁的客户端还存活并且能够与Redis通信的情况下才会续期。如果客户端崩溃,那么续期操作就会停止,锁就会自动释放。 总结一下,Redisson锁续期机制检测线程结束或崩溃的方式: - 对于同一个JVM内的线程结束:通过检查线程状态(isAlive())来停止续期。 - 对于客户端崩溃(如JVM崩溃):看门狗线程也会停止,因此不会续期,锁自动过期。 需要注意的是,锁的默认过期时间是30秒,看门狗线程每10秒(即30/3)续期一次,将锁的过期时间重置为30秒。 下面是一个简化的流程: 1. 线程A获取锁成功,Redisson启动一个看门狗线程(如果还没有的话)。 2. 看门狗线程每隔10秒检查一次: a. 检查当前线程(线程A)是否还持有该锁(通过比较Redis中存储的锁的值,该值包含了线程ID等信息)并且线程A是否还存活。 b. 如果线程A还持有锁且线程A还存活,则执行续期操作(将锁的过期时间重置为30秒)。 c. 如果线程A已经不再持有锁(比如被其他线程释放了)或者线程A已经结束(isAlive()返回false),则不再续期,并结束看门狗线程。 3. 如果线程A在持有锁期间崩溃(整个JVM崩溃),那么看门狗线程也会停止,因此不会续期,锁在30秒后自动过期。 这样,就避免了因为持有锁的线程崩溃而导致锁一直不被释放的问题。 引用[^1]提到了Radisson(应为Redisson)的看门狗机制,它类似于AQS的实现(在分布式环境中的实现),通过后台线程来监听锁的状态并续期。 因此,用户的问题得到解答:Redisson通过看门狗线程定期检查持有锁的线程是否存活(在同一个JVM内)以及通过续期操作的停止(在客户端崩溃时)来检测线程结束或崩溃。 现在,我们根据用户的问题,生成相关问题:</think>### Redisson 锁续期机制解析 Redisson 的锁续期机制(看门狗机制)通过以下方式检测线程结束或崩溃: 1. **线程状态监控** Redisson 在获取锁时为当前线程注册一个监控任务(看门狗线程),定期(默认10秒)检查持有锁的线程状态: - 通过 Java 的 `Thread.isAlive()` 方法判断线程是否存活 - 若线程已终止(`isAlive() == false`),立即停止续期并释放锁 - 示例代码逻辑: ```java if (!Thread.currentThread().isAlive()) { unlock(); // 释放锁 cancelRenewal(); // 停止续期 } ``` 2. **JVM 崩溃检测** 当整个 JVM 崩溃时: - 看门狗线程随进程终止而销毁 - Redis 中的锁 key 因**未续期而自动过期**(默认30秒) - 其他线程可在超时后重新获取锁 3. **续期失败处理** 续期操作通过 Redis Lua 脚本实现原子性验证: ```lua if redis.call("hexists", KEYS[1], ARGV[2]) == 1 then redis.call("pexpire", KEYS[1], ARGV[1]); return 1; end; return 0; ``` - 续期前验证线程ID是否匹配 - 若线程崩溃导致ID不匹配,续期失败触发锁释放 4. **双重超时保障** - **锁租约时间**:默认30秒,Redis 自动过期释放 - **续期间隔**:默认10秒(租约时间的1/3) - 任一超时均可触发锁释放 [^1] 引用说明:Redisson 通过线程状态检测和租约超时双重机制保障锁释放,避免因线程崩溃导致的死锁问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值