当前位置: 首页 > news >正文

【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 | 第五篇】分布式锁

在上一篇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]);

三种情况对应三条分支:

  1. 锁不存在→ 创建 Hash,field=线程标识,value=1,设置过期时间
  2. 锁存在且 field 匹配→ 这是重入!value+1,刷新过期时间
  3. 锁存在但 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 │ └─────────────────────────────────────────────────────┘

关键设计点

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

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

  3. 剩余时间精确计算:每次被唤醒后重新尝试加锁,并重新计算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(); }
http://www.jsqmd.com/news/939400/

相关文章:

  • ComfyUI-VideoHelperSuite视频处理模块零除错误深度解析与技术方案
  • 618选游戏本不知道怎么选?这5款覆盖不同需求,附详细选购建议
  • AI工具≠深度学习加速器!3小时重构你的训练-推理-监控流水线(附GitHub万星整合模板)
  • 5分钟掌握微信好友检测:快速发现谁删除了你
  • 【2027最新】基于SpringBoot+Vue的医院资源管理系统管理系统源码+MyBatis+MySQL
  • 2026年浙江正规钻井服务评测:四家企业核心维度对比 - 优质品牌商家
  • ## 南山罗湖福田龙华宝安装修必看:ENF定制套餐挑选的核心判断标准 - 产品测评官
  • 视觉语言模型量化与剪枝技术解析
  • 亚马逊卖家必看:为什么说AI商品套图正在淘汰传统海外商拍?
  • 选购无人机操作培训考证服务,鲲鹏翼航口碑好 - mypinpai
  • 量子计算基础:原理、算法与NISQ时代应用
  • RoLA框架:单图像驱动的机器人交互场景物理仿真
  • 数字世界的“骨架构建师”:3D结构建模软件市场深度分析与未来展望
  • STC89C52三路抢答器全套开发资料:Keil工程+Proteus仿真+可烧录hex文件(共阳数码管)
  • 杰理之耳机进入powerdown后,电平跟随powerdown跳动【篇】
  • 冥想第一千八百九十八天(1898)
  • 成都大型储水桶水塔:成都塑料圆盆水箱水塔/成都塑料方水塔/成都塑料水塔/成都工业塑料水塔/成都工地储水塔/选型 - 优质品牌商家
  • 露营改装智己ls9选购技巧 - mypinpai
  • ATF-BL1启动流程详解:从复位到BL2的完美一跳
  • FPGA加速Mamba推理:SpecMamba方案与优化实践
  • Windows 10/11下保姆级教程:用QEMU 8.2.0跑通OpenHarmony 4.1(ARM Cortex-M4版)
  • 如何三分钟搞定黑苹果:OpCore-Simplify终极自动化配置指南
  • VC6环境下可直接编译的IEC104主从站双模仿真工具包
  • 有实力的建筑公司代理记账机构 - mypinpai
  • 告别Unity启动Logo卡顿:深入SplashScreen.Stop与RuntimeInitializeOnLoadMethod的保姆级教程
  • 微软更新、360广告与火绒误杀:一场导致Win10黑屏的‘三角债’技术复盘
  • 主流 AI 语言模型横向大盘点:普通人日常办公、写文章到底该怎么选?
  • 你的聊天数据,你真正做主:WeChatMsg微信聊天记录永久保存完全指南
  • 告别复杂调参:用Google的FixMatch算法,5行代码搞定你的半监督图像分类项目
  • CyQuantiFluor™细胞活力检测试剂盒检测原理详解