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

分布式锁从Redis到Redisson的演进

在分布式架构下,传统的 JVM 锁(如synchronized)无法跨进程生效。为了保证共享资源(如秒杀库存、数据库行记录)的并发安全,我们需要借助 Redis 实现分布式锁。以下是分布式锁在:

解决实际问题中的四个关键演进阶段

1. 基础实现:解决“原子性”与“死锁”

最直观的实现是利用 Redis 的SETNX(SET if Not Exists)命令。

  • 初级陷阱:如果只执行SETNX加锁,一旦客户端宕机,锁将永远无法释放,导致系统死锁。
  • 优化方案:必须为锁设置过期时间(TTL)。同时,为了保证“加锁”和“设置过期时间”的原子性,避免中间宕机引发死锁,应直接使用官方推荐的扩展命令:
    SET key value NX EX seconds
    (即:仅当键不存在时设置值,并附带过期秒数)。
2. 安全升级:解决“误删锁”
  • 进阶陷阱:如果业务执行时间超过了锁的过期时间,锁会自动释放。此时,线程 A 执行完毕后去释放锁,可能会误删掉线程 B 刚刚获取到的新锁。
  • 优化方案
    1. 唯一标识:加锁时,将 Value 设置为唯一标识(如UUID + ThreadID)。
    2. Lua 脚本原子删锁:释放锁时,先校验 Value 是否匹配,匹配再删除。这两个动作必须封装在 Lua 脚本中执行,确保原子性。
3. 高可用挑战:解决“主从切换丢锁”
  • 架构陷阱:在 Redis 主从(Master-Slave)架构下,锁的同步是异步的。如果客户端 A 在主节点加锁成功,但主节点还没来得及将数据同步给从节点就宕机了,从节点晋升为新主节点后,客户端 B 依然能成功加锁。这会导致多个客户端同时持有锁,互斥性失效。
  • 优化方案:引入Redlock(红锁)算法。摒弃传统的主从架构,向多个完全独立的 Redis 主节点同时申请锁,只有当超过半数(N/2 + 1)的节点加锁成功,才算真正获取到分布式锁,从而规避单点故障带来的锁丢失风险。
4. 生产落地:解决“业务超时”与“代码复杂度”
  • 终极陷阱
    1. 业务执行时间难以精准预估,固定超时时间容易导致锁提前释放。
    2. 手写包含 Lua 脚本、唯一标识、Redlock 等逻辑的代码极其复杂,且容易遗漏边界情况(如锁的可重入性)。
  • 最终方案:直接使用成熟的框架Redisson
    • 看门狗(WatchDog)机制:Redisson 会在后台自动为未执行完的业务续期,彻底解决“业务超时导致锁提前释放”的难题。
    • 开箱即用:它原生支持可重入锁、公平锁、红锁以及底层的 Lua 脚本原子操作,是生产环境中低成本、高稳定的标准答案。

简单理解为四个阶段

阶段一:初窥门径(单机 SETNX)
  • 场景:单体应用拆分为分布式系统,synchronized失效。
  • 方案:使用 Redis 的SETNX命令。
  • 痛点:如果业务崩溃,锁无法释放(死锁)。
阶段二:修补漏洞(原子性与防误删)
  • 演进:引入SET key value NX PX原子操作。
  • 新问题:锁过期了,业务还没执行完;或者业务执行完了去删除锁,结果删掉了别人刚获取的新锁。
  • 方案:引入唯一Value(UUID)+ Lua 脚本(保证“判断-删除”原子性)。
阶段三:高可用挑战(Redlock 与 主从一致性)
  • 演进:单点 Redis 不可靠,引入主从。
  • 新问题:异步复制导致的锁丢失(主节点挂掉,从节点未同步锁)。
  • 方案:探讨 Redlock 算法(多节点投票),引出其运维复杂性。
阶段四:成熟落地(Redisson 看门狗)
  • 演进:手写代码维护成本太高,且难以兼顾性能与可靠性。
  • 终局:引入Redisson
    • 看门狗(WatchDog):自动续期,解决业务超时问题。
    • 可重入:基于 Hash 结构记录线程 ID 和重入次数。
    • 发布订阅:优化锁等待机制,减少无效轮询。

补充说明

加锁前生成唯一 UUID 的含义

在分布式系统中,锁的 Key 通常使用业务 ID(如用户 ID、单据 ID),但生成唯一 UUID 仍有其必要性:
避免锁冲突
业务 ID 可能重复使用(如订单取消后重新创建)。UUID 作为临时唯一标识,确保锁的独立性。

锁的粒度控制
业务 ID 可能对应多个操作(如支付和退款)。通过拼接 UUID 可细分锁粒度,例如:order_12345_refund_abcd-uuid

防止误释放
线程 A 获取锁后若超时,锁可能被自动释放并被线程 B 获取。若线程 A 恢复后直接使用业务 ID 释放锁,会误删线程 B 的锁。附加 UUID 可校验锁的归属。

业务 ID 作为 Key 的潜在问题

  • 锁覆盖风险:高并发下,相同业务 ID 的多个请求可能竞争同一把锁,导致非预期阻塞或锁失效。
  • 锁续期混淆:自动续期机制可能因业务 ID 重复而错误延长其他线程的锁。

推荐实践

  • 复合 Key 结构:结合业务 ID 和 UUID:lock:user_12345:3a4b5c6d,兼顾业务语义与唯一性。
  • 锁值设计:存储 UUID 作为锁的值,释放时校验一致性,避免误操作。
  • 超时与续约:设置合理超时时间,并通过 UUID 标识续约归属,避免死锁。

代码示例(Redis 锁)

// 加锁StringbusinessKey="order_12345";StringlockId=UUID.randomUUID().toString();booleanlocked=redis.set(businessKey,lockId,"NX","EX",30);// 释放锁if(lockId.equals(redis.get(businessKey))){redis.del(businessKey);}

使用 Redisson 之后,你不需要再手动进行 UUID 的比对,Redisson 已经在底层帮你完美封装并自动处理了这一切。

至于是否需要指定过期时间,这与 UUID 的自动比对没有直接关系,但会直接影响 Redisson 的另一个核心机制——看门狗(Watchdog)的自动续期


问题拓展

1. 为什么用了 Redisson 就不需要手动比对 UUID 了?

在手动实现分布式锁时,我们需要生成 UUID 并存入 Redis,释放锁时再通过 Lua 脚本比对 UUID,核心目的是防止“误删他人的锁”

Redisson 在底层已经原生实现了这一整套安全机制:

  • 自动绑定唯一标识:当你调用 Redisson 的加锁方法时,它会自动生成一个全局唯一的标识(格式通常为UUID:线程ID),并将其作为锁的 Value 存入 Redis。
  • 自动校验与释放:当你调用unlock()释放锁时,Redisson 会在内部执行一段封装好的 Lua 脚本。这段脚本会自动去 Redis 中校验当前锁的标识是否属于当前线程。只有校验通过,它才会真正删除这把锁

因此,使用 Redisson 时,你只需要简单地调用lock.lock()lock.unlock(),完全不用操心 UUID 的生成、存储和比对,它已经帮你规避了“误删锁”的风险。

2. 是否指定过期时间,会有什么影响?

指定过期时间不会改变 Redisson “自动比对 UUID” 的安全特性,但它直接决定了看门狗(Watchdog)机制是否会启动

我们可以分两种情况来看:

  • 情况一:不指定过期时间(推荐)

    • 写法lock.lock()
    • 表现:当你没有显式指定锁的过期时间时,Redisson 会默认给锁设置一个 30 秒的过期时间,并自动启动看门狗机制
    • 看门狗的作用:看门狗会在后台每隔 10 秒(默认 30 秒的 1/3)检查一次,如果你的业务还没执行完且依然持有这把锁,它就会自动把锁的过期时间重新刷新回 30 秒。这完美解决了“业务执行时间超过锁过期时间”导致的并发安全问题。
  • 情况二:显式指定了过期时间

    • 写法lock.lock(10, TimeUnit.SECONDS)
    • 表现:一旦你手动指定了过期时间(比如 10 秒),Redisson 就会认为你非常清楚自己的业务耗时,此时看门狗机制将不会启动
    • 潜在风险:锁会在 10 秒后强制过期释放。如果你的业务逻辑执行超过了 10 秒,锁就会被自动释放,其他线程可能会抢到这把锁,从而引发并发冲突。

为了更直观地理解,可以参考下表:

加锁方式看门狗机制适用场景
lock.lock()(不指定时间)自动启动(默认30s,每10s续期)核心业务,无法准确预估耗时的场景
lock.lock(10, TimeUnit.SECONDS)(指定时间)不会启动耗时极短且非常确定的操作(如秒杀扣库存)
http://www.jsqmd.com/news/767837/

相关文章:

  • 2026年知名的鹤壁婚房装修/鹤壁旧房装修热选公司推荐 - 品牌宣传支持者
  • 开源数字永生框架实践:四维蒸馏构建AI数字分身
  • 开源IVD数据管理工具:从数据孤岛到标准化分析的实践指南
  • Anthropic Claude API用户代理插件:伪装请求头绕过限制与优化调用
  • 从零构建开源机械爪:ESP32控制与3D打印实践指南
  • 深度学习与地图增强代理技术在图像地理定位中的应用
  • 零基础吃透 Java 面向对象:类、对象、this 与 static 实战
  • 硬件设计避坑:PMOS缓启动电路关断慢?实测教你优化栅极泄放回路(含仿真文件)
  • Banana Pi BPI-Leaf-S3开发板硬件解析与AI应用开发
  • NS模拟器管理困境的终结者:NsEmuTools如何重塑你的游戏体验
  • 观察者模式是行为型设计模式的一种,其核心思想是定义对象间的一对多依赖关系
  • PE-bear:免费PE文件分析神器,让Windows逆向工程变得简单快速
  • 从0到1掌握反反爬:IP封禁与UA检测的底层原理及工业级突破方案
  • 打破数据黑盒依赖困境,镜像视界开创可信孪生时代
  • 别再为调试器发愁了!手把手教你用OpenOCD搞定J-Link、ST-Link和FTDI
  • 千问 LeetCode 2122.还原原数组 public int[] recoverArray(int[] nums)
  • 移植代码后LED灯都不闪了?可能是VTOR向量表地址在捣鬼(附STM32CubeIDE与Keil排查步骤)
  • Ising机与Bounce-Bind机制在组合优化中的应用
  • 构建可移植开发环境:配置仓库与自动化部署实践
  • 机械操作耗尽精力?dothething:一款全自主本地 AI 代理,替你接管系统控制与网络任务
  • RTX 3050 + Win11实测:Python 3.10环境下,用pip搞定TensorFlow-GPU 2.10.1的完整避坑指南
  • OpencvSharp 算子学习教案之 - Cv2.GetStructuringElement 重载1
  • STM32F103C8T6硬件SPI驱动W25Q64 Flash全流程(附完整工程代码)
  • C#基础(持续更新中)
  • Python初学者项目练习9--对简单列表元素排序
  • 解决ZYNQ裸机网络扩展难题:为LWIP库添加自定义PHY驱动与SDK配置界面
  • Windows系统光标深度替换:INF方案实现macOS指针移植与优化
  • AI编码助手统一配置工具agent-dotfiles:告别重复配置,实现规则与技能一键同步
  • BrowserClaw:基于Puppeteer与Playwright的浏览器自动化与数据抓取实践
  • AI工具搭建自动化视频生成图像缩放