欢迎光临
我们一直在努力

【Redis | 第六篇】Redisson

目录

一、前言

二、解决不可重入:Hash 数据结构 + Lua 原子脚本

2.1 不可重入的问题在哪?

2.2 Redisson 的解决方案:Redis Hash

2.3 加锁 Lua 脚本详解

2.4 解锁 Lua 脚本

三、解决不可重试:消息订阅 + 信号量等待

3.1 简单方案的缺陷

四、解决超时释放:看门狗(Watchdog)自动续期

4.1 为什么需要续期?

4.2 Watchdog 看门狗机制

4.3 看门狗工作原理

五、MultiLock 联锁:跨 Redis 实例的锁聚合

5.1 背景:主从一致性问题的延伸

5.2 RedissonMultiLock 的设计思想

5.3 使用示例


一、前言

        在上一篇Redis实现分布式锁,我们是基于SET NX EX命令来实现的简单的分布式锁,虽然上手容易,但是在生产环境中还存在以下问题:

痛点问题描述
不可重入 同一个线程在持有锁的情况下,再次获取同一把锁会死锁
不可重试 获取锁失败后立刻返回 false,无法自动重试,调用方只能自旋
超时释放 业务还没执行完,锁就过期了,导致并发安全问题
主从一致性 Redis 主节点宕机,从节点还没同步锁数据,导致锁丢失

        Redisson 作为 Java 生态中最强大的 Redis 客户端,对分布式锁做了非常完善的封装。它不仅实现了 java.util.concurrent.locks.Lock 接口,还通过精巧的设计把这四个问题一一化解。

二、解决不可重入:Hash 数据结构 + Lua 原子脚本

2.1 不可重入的问题在哪?

        传统的 SET NX EX 方案中,锁就是一个简单的 String key,只有 "存在/不存在" 两种状态。同一个线程如果想再次获取同一把锁(比如递归调用或嵌套方法),会因为 key 已存在而直接失败——这就是死锁的根源。

        Java 中的 ReentrantLock 是通过 state 变量记录重入次数的:state=0 表示无锁,加锁时 state+1,释放时 state-1,直到归零才真正释放。

        Redisson 借鉴了这个思想,但在 Redis 中需要一个能同时存储 "谁持有了锁" 和 "重入了多少次" 的数据结构。

2.2 Redisson 的解决方案:Redis Hash

Redisson 使用 Redis 的 Hash 结构来存储锁信息:

Key: 锁的名称 → "myLock"
Field: 线程标识 → "连接ID:线程ID" (如 "uuid-123:thread-42")
Value: 重入次数 → 1, 2, 3 …

示意图:

┌──────────────────────────────────┐
│ Key: "myLock" (Hash) │
│ ┌────────────────────────────┐ │
│ │ Field: "uuid-xxx:thread-1" │ │
│ │ Value: 2 (重入了2次) │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘

2.3 加锁 Lua 脚本详解

Redisson 将加锁逻辑封装在一条 Lua 脚本中,利用 Redis 执行 Lua 脚本的原子性保证并发安全:

— KEYS[1]: 锁的名称,如 "myLock"
— ARGV[1]: 锁的过期时间,默认 30000 毫秒
— ARGV[2]: 线程标识,格式为 "连接ID:线程ID"

— 情况1: 锁不存在 → 直接加锁
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1); — 设置重入次数为1
redis.call('pexpire', KEYS[1], ARGV[1]); — 设置过期时间
return nil; — 返回nil表示加锁成功
end;

— 情况2: 锁存在,且是当前线程持有 → 重入
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1); — 重入次数+1
redis.call('pexpire', KEYS[1], ARGV[1]); — 刷新过期时间
return nil; — 返回nil表示加锁成功
end;

— 情况3: 锁被别人持有 → 返回剩余TTL
return redis.call('pttl', KEYS[1]);

三种情况对应三条分支:

  • 锁不存在 → 创建 Hash,field=线程标识,value=1,设置过期时间
  • 锁存在且 field 匹配 → 这是重入!value+1,刷新过期时间
  • 锁存在但 field 不匹配 → 被别人持有,返回剩余存活时间(ms)
  • 2.4 解锁 Lua 脚本

    释放锁也是通过 Lua 脚本原子执行:

    — 判断锁是否被当前线程持有
    if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    return nil; — 不是你的锁,不能释放
    end;

    — 重入次数-1
    local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);

    if (counter > 0) then
    — 还有重入层数,只刷新过期时间,不删除key
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 0;
    else
    — 重入次数归零,彻底删除锁
    redis.call('del', KEYS[1]);
    — 发布解锁消息,通知等待队列中的线程
    redis.call('publish', KEYS[2], ARGV[1]);
    return 1;
    end;

            解锁时 counter>0 说明还有嵌套层没释放完,只减计数不删 key——这就是可重入锁的释放逻辑。

    三、解决不可重试:消息订阅 + 信号量等待

    3.1 简单方案的缺陷

    原生 SET NX 加锁失败后,通常的做法是让线程 sleep 一段时间再重试。但这样有两个问题:

    • CPU 空转,浪费资源
    • sleep 时间不好把握:太短则频繁重试,太长则响应慢

    核心流程如下:

    ┌─────────────────────────────────────────────────────┐
    │ tryLock(waitTime, leaseTime, unit) │
    │ │ │
    │ ▼ │
    │ 尝试获取锁 (执行 Lua 脚本) │
    │ │ │
    │ ├── 成功 → 返回 true │
    │ │ │
    │ └── 失败,拿到锁的剩余 TTL │
    │ │ │
    │ ▼ │
    │ 计算剩余等待时间 = waitTime – 已消耗时间 │
    │ │ │
    │ ├── 剩余时间 ≤ 0 → 返回 false (超时放弃) │
    │ │ │
    │ └── 剩余时间 > 0 → │
    │ │ │
    │ ▼ │
    │ 订阅一个 Redis Channel (锁名对应的频道) │
    │ │ │
    │ ▼ │
    │ 通过信号量(Semaphore)阻塞等待 │
    │ await(time, TimeUnit) │
    │ │ │
    │ ├── 收到解锁消息 → 被唤醒,回到「尝试获取锁」 │
    │ │ │
    │ └── 等待超时 → 返回 false │
    └─────────────────────────────────────────────────────┘

    关键设计点:

  • Redis Pub/Sub:当持有锁的线程释放锁时(Lua 脚本中执行 publish),所有订阅该频道的等待线程会被唤醒,避免了无意义的轮询。

  • 信号量(Semaphore):Redisson 在 Java 侧使用 Semaphore 让线程阻塞等待。await(time, TimeUnit) 方法支持超时唤醒,与 waitTime 完美配合。

  • 剩余时间精确计算:每次被唤醒后重新尝试加锁,并重新计算 waitTime – 已消耗时间,确保总等待时间不超过用户指定的 waitTime

  • 四、解决超时释放:看门狗(Watchdog)自动续期

    4.1 为什么需要续期?

    传统分布式锁一定会设置过期时间,防止客户端宕机导致死锁。但问题在于:你无法预知业务代码会执行多久。

    假设锁的过期时间 = 10秒
    业务执行时间 = 12秒

    时间线:
    0s ─── 加锁成功

    10s ─── 锁自动过期!(Redis 删除了 key)
    10s ─── 另一个线程拿到了锁

    12s ─── 第一个线程业务执行完毕,但它持有的"锁"实际上已经失效
    → 并发安全问题!

    4.2 Watchdog 看门狗机制

    Redisson 的解决方案是 Watchdog(看门狗)自动续期机制。

    核心规则:只有不指定 leaseTime(或设为 -1)时,才会启动看门狗。

    // 不指定leaseTime → 触发看门狗,默认30秒过期,自动续期
    lock.lock();

    // 指定了leaseTime → 不会触发看门狗,到期自动释放
    lock.lock(10, TimeUnit.SECONDS);

    4.3 看门狗工作原理

    ┌──────────────────────────────────────────────────┐
    │ 1. 加锁成功(leaseTime = -1) │
    │ │ │
    │ ▼ │
    │ 2. 设置默认过期时间 = 30秒 (lockWatchdogTimeout) │
    │ │ │
    │ ▼ │
    │ 3. 启动定时任务 (Netty Timer / ScheduledExecutor) │
    │ │ │
    │ ▼ │
    │ 4. 每 30/3 = 10秒 执行一次续期 │
    │ ┌─────────────────────────────┐ │
    │ │ Lua 脚本: │ │
    │ │ if 锁存在且是当前线程持有 │ │
    │ │ → pexpire KEY 30000 │ │
    │ │ → 重置过期时间为30秒 │ │
    │ └─────────────────────────────┘ │
    │ │ │
    │ ▼ │
    │ 5. 循环执行,直到客户端主动 unlock │
    │ │ │
    │ ▼ │
    │ 6. unlock 时:取消定时任务 + 删除锁 │
    └──────────────────────────────────────────────────┘

    五、MultiLock 联锁:跨 Redis 实例的锁聚合

    5.1 背景:主从一致性问题的延伸

    即使有了看门狗,单节点 Redis 仍然存在单点故障风险。如果使用 Redis 主从 + Sentinel 哨兵模式:

    客户端A 在主节点获取锁成功

    主节点宕机,数据还没同步到从节点

    哨兵将从节点提升为新主节点

    客户端B 在新主节点获取同一把锁 → 成功!

    客户端A 和 B 同时持有同一把锁 → 灾难!

    5.2 RedissonMultiLock 的设计思想

            Redisson 提供了 MultiLock(联锁),可以将多个独立的 RLock 合并成一个逻辑上的"大锁"。每个 RLock 可以来自不同的 Redis 节点,只有当所有子锁都加锁成功时,MultiLock 才算加锁成功。

    ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
    │ Redis Node 1 │ │ Redis Node 2 │ │ Redis Node 3 │
    │ lock1 ✓ │ │ lock2 ✓ │ │ lock3 ✓ │
    └──────────────┘ └──────────────┘ └──────────────┘
    │ │ │
    └─────────────────┼─────────────────┘

    ┌──────▼──────┐
    │ MultiLock │
    │ 全部成功 → │
    │ 加锁成功 │
    └─────────────┘

    5.3 使用示例

    // 三个不同的 Redis 实例
    RLock lock1 = redissonInstance1.getLock("myLock");
    RLock lock2 = redissonInstance2.getLock("myLock");
    RLock lock3 = redissonInstance3.getLock("myLock");

    // 合并为联锁
    RLock multiLock = redisson.getMultiLock(lock1, lock2, lock3);

    // 使用方式和普通锁完全一致
    multiLock.lock();
    try {
    // 业务逻辑
    } finally {
    multiLock.unlock();
    }

    赞(0)
    未经允许不得转载:171主机测评 » 【Redis | 第六篇】Redisson
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址