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

发布于:2025-05-17 ⋅ 阅读:(22) ⋅ 点赞:(0)
一、可重入锁(Reentrant Lock)

可重入锁是什么?

  • 通俗定义

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

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

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

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。
      -- 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。
      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。

    -- 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}
    • 事件驱动优化:避免频繁轮询,仅当锁释放时触发重试,减少无效请求
    // 伪代码:订阅锁释放事件
    RFuture<RedissonLockEntry> future = subscribe(lockName);
    RedissonLockEntry entry = get(future);
    
  3. 循环重试(主动轮询 + 事件触发)

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

    关键代码逻辑(简化)

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 秒(默认值)。

      -- 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. 配置示例

    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:向多个独立节点加锁,需半数以上成功;解锁时向所有节点发送命令,解决主从异步复制导致的锁失效。

网站公告

今日签到

点亮在社区的每一天
去签到