Redisson 四大核心机制实现原理详解

一、可重入锁(Reentrant Lock)

可重入锁是什么?

  • 通俗定义

    可重入锁类似于一把"智能锁",它能识别当前的锁持有者是否是当前线程:

    • 如果是,则允许线程重复获取锁(重入),并记录重入次数。
    • 如果不是,则其他线程必须等待锁释放后才能获取。
  • 典型场景

    当一个线程调用了一个被锁保护的方法A,而方法A内部又调用了另一个被同一锁保护的方法B时,如果锁不可重入,线程会在调用方法B时被自己阻塞(死锁)。可重入锁允许这种嵌套调用。

java 复制代码
public class Demo {
    private final Lock lock = new SomeLock(); // 假设这是一个锁

    public void methodA() {
        lock.lock();
        try {
            methodB(); // 调用另一个需要加锁的方法
        } finally {
            lock.unlock();
        }
    }

    public void methodB() {
        lock.lock();
        try {
            // 业务逻辑
        } finally {
            lock.unlock();
        }
    }
}
  • 如果锁不可重入 线程进入methodA获取锁后,调用methodB时再次尝试加锁,会因为锁已被自己持有而永久阻塞(死锁)。
  • 如果锁可重入 线程在methodB中能成功获取锁,计数器从1增加到2,释放时计数器递减,最终正常释放。

实现原理 :通过 Redis 的 Hash 结构实现线程级锁的可重入性。

  1. 数据结构

    • Key :锁名称(如 lock:order:1001)。
    • Field :客户端唯一标识(UUID + 线程ID),如 b983c153-7091-42d8-823a-cb332d52d2a6:1
    • Value :锁的 重入次数(初始为 1,重入时递增)。
  2. 加锁逻辑

    • 首次加锁 :执行 Lua 脚本,若 Key 不存在,创建 Hash 并设置重入次数为 1。

      lua 复制代码
      -- KEYS[1]=锁名, ARGV[1]=锁超时时间, ARGV[2]=线程唯一ID
      if (redis.call('exists', KEYS[1]) == 0) then  	 -- 如果锁不存在
          redis.call('hincrby', KEYS[1], ARGV[2], 1);  -- 创建Hash,记录线程重入次数
          redis.call('pexpire', KEYS[1], ARGV[1]);	 -- 设置锁超时时间
          return nil;									 -- 返回成功
      end;
    • 重入加锁 :若 Field 匹配当前线程,重入次数 +1。

      lua 复制代码
      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;
  3. 释放锁:减少重入次数,归零时删除 Hash。

    lua 复制代码
    -- KEYS[1]: 锁名称(如 my_lock)
    -- KEYS[2]: 发布订阅的频道名
    -- ARGV[1]: 解锁消息标识(如 0)
    -- ARGV[2]: 锁的过期时间(毫秒)
    -- ARGV[3]: 客户端唯一标识(UUID + 线程ID)
    
    -- 检查锁是否存在且属于当前线程
    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; -- 返回0表示未完全释放
    else
        -- 计数器归零,删除锁并发布释放通知
        redis.call('del', KEYS[1]);
        redis.call('publish', KEYS[2], ARGV[1]);
        return 1; -- 返回1表示锁已完全释放
    end;

二、锁重试机制(Retry Mechanism)

重试机制的触发条件

当调用 tryLock(long waitTime, long leaseTime, TimeUnit unit) 方法时,若 waitTime > 0,Redisson 会启用重试机制。例如:

复制代码
java// 10秒内不断重试获取锁,获取成功后持有锁60秒
lock.tryLock(10, 60, TimeUnit.SECONDS);

若首次获取锁失败,进入重试流程。

实现原理: 事件驱动优先,主动轮询兜底

  1. 首次尝试获取锁

    • 原子性操作:通过 Lua 脚本尝试获取锁(检查锁是否存在或是否属于当前线程)。
    • 失败返回值 :若锁被其他线程持有,返回锁的剩余存活时间(ttl)。
  2. 订阅锁释放事件

    • 创建监听频道 :订阅 Redis 频道 redisson_lock__channel:{lockName}
    • 事件驱动优化 :避免频繁轮询,仅当锁释放时触发重试,减少无效请求
    java 复制代码
    // 伪代码:订阅锁释放事件
    RFuture<RedissonLockEntry> future = subscribe(lockName);
    RedissonLockEntry entry = get(future);
  3. 循环重试(主动轮询 + 事件触发)

    • 计算剩余等待时间 :基于 waitTime 和已消耗时间,动态调整剩余等待窗口。
    • 双重检测逻辑
      • 主动轮询:定期(默认间隔 100ms ~ 300ms)执行 Lua 脚本尝试获取锁。
      • 事件触发:收到锁释放通知后立即尝试获取锁。
    • 退避策略:每次重试失败后,采用随机递增的等待时间(避免多个客户端同时竞争导致雪崩)。

    关键代码逻辑(简化)

java 复制代码
long remainingTime = waitTime; // 剩余等待时间
long startTime = System.currentTimeMillis();

while (remainingTime > 0) {
    // 1. 尝试获取锁
    Long ttl = tryAcquire(leaseTime, unit); // 调用Lua脚本
    if (ttl == null) {
        return true; // 获取成功
    }

    // 2. 计算剩余时间
    long elapsed = System.currentTimeMillis() - startTime;
    remainingTime -= elapsed;

    if (remainingTime <= 0) {
        break; // 超时退出
    }

    // 3. 等待锁释放事件或超时
    entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); // 基于信号量等待

    // 4. 更新剩余时间
    remainingTime -= (System.currentTimeMillis() - startTime - elapsed);
}
return false; // 超时未获取
  1. 超时终止
    • 时间窗口耗尽 :若总耗时超过 waitTime,终止重试并返回失败。
    • 资源清理:取消 Redis 订阅,释放连接。

三、WatchDog 看门狗(锁续期机制)

防止业务执行时间超过锁的过期时间,导致锁提前释放。

启用看门狗需满足以下条件之一:

  • 未显式指定锁的租约时间(leaseTime) : 例如调用 lock.tryLock()lock.lock() 时不传 leaseTime 参数。
  • 显式设置租约时间为 -1 : 例如 lock.tryLock(10, -1, TimeUnit.SECONDS)

注意 :若指定了固定的 leaseTime(如 lock.tryLock(10, 30, TimeUnit.SECONDS)),看门狗不会启动,锁会在 30 秒后自动释放。

实现原理:后台线程自动续期锁,防止业务未完成时锁过期。

  1. 触发条件 :未指定锁超时时间(如 lock.lock())。

  2. 续期逻辑

    • 定时任务 :默认每 10 秒(lockWatchdogTimeout / 3)续期一次。

    • 续期命令:重置锁的过期时间为 30 秒(默认值)。

      lua 复制代码
      -- KEYS[1]: 锁名称
      -- ARGV[1]: 过期时间(默认30秒)
      -- ARGV[2]: 客户端唯一标识
      if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
          redis.call('pexpire', KEYS[1], ARGV[1]);
          return 1;
      end;
      return 0;
  3. 终止条件

    • 锁被释放(unlock() 调用)。
    • 客户端断开连接或线程中断。

四、主从一致性(MultiLock/RedLock)

Redis 主从复制是异步的,若主节点宕机且锁未同步到从节点,可能导致多个客户端同时持有锁。

实现原理:基于多数派原则,向多个独立节点加锁。

  1. MultiLock 流程

    • 加锁 :向所有节点发送加锁请求,需 半数以上成功(如 3 节点至少 2 个成功)。
    • 容错 :允许最多 ⌊(N-1)/2⌋ 个节点故障(如 5 节点允许 2 个故障)。
    • 解锁:无论加锁是否成功,向所有节点发送解锁命令。
  2. RedLock 算法增强

    • 时钟同步:要求节点使用 NTP 同步时间,锁有效期需包含时钟漂移。
    • 加锁验证:计算加锁耗时,确保有效时间未耗尽。
  3. 配置示例

    java 复制代码
    RLock lock1 = redissonClient1.getLock("lock");
    RLock lock2 = redissonClient2.getLock("lock");
    RLock multiLock = new RedissonMultiLock(lock1, lock2);
    multiLock.lock();
    try {
        // 业务逻辑
    } finally {
        multiLock.unlock();
    }

五、总结
机制 实现原理
可重入锁 使用 Redis Hash 结构存储锁名、线程唯一标识(UUID+线程ID)和重入次数。同一线程多次获取锁时重入次数递增,释放时递减,归零后删除锁。
锁重试 通过 Pub/Sub 订阅锁释放事件 避免轮询;失败后按退避策略(默认 1.5 秒)重试,直到超时或成功。
WatchDog 后台线程每 10 秒(默认)检查锁持有状态,若锁存在则续期(重置过期时间至 30 秒)。未指定锁超时时间时自动启用。
主从一致性 使用 MultiLock/RedLock:向多个独立节点加锁,需半数以上成功;解锁时向所有节点发送命令,解决主从异步复制导致的锁失效。
相关推荐
牛奶咖啡1317 小时前
Nginx+Tomcat集群Redis共享session方案
redis·nginx·tomcat·redisson·分布式session共享方案·分布式session实现·jdk1.8环境安装
蜡笔小柯南5 天前
每秒扛住10万请求?RedissonRateLimiter 分布式限流器详解
分布式·redisson·滑动窗口·ratelimiter
秃了也弱了。20 天前
Redisson最新版本(3.50.0左右)启动时提示Netty的某些类找不到
redisson
会编程的林俊杰1 个月前
Redisson中的分布式锁
redis·分布式·redisson
鼠鼠我捏,要死了捏1 个月前
基于Redisson实现高并发分布式锁性能优化实践指南
性能优化·分布式锁·redisson
C182981825752 个月前
Redisson加锁脚本分析
redisson
C182981825752 个月前
Redisson解锁脚本分析
redisson
phantomsee2 个月前
Redis学习系列之——高并发应用的缓存问题(二)
redis·redisson
马里奥Marioぅ2 个月前
Redis主从切换踩坑记:当Redisson遇上分布式锁的“死亡连接“
redis·分布式锁·redisson·故障转移
xujinwei_gingko2 个月前
接口幂等性
分布式锁·redisson·接口幂等性