分布式锁从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 锁)

java 复制代码
// 加锁
String businessKey = "order_12345";
String lockId = UUID.randomUUID().toString();
boolean locked = 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) (指定时间) 不会启动 耗时极短且非常确定的操作(如秒杀扣库存)
相关推荐
山峰哥2 小时前
SQL性能提升20倍的秘密:这些优化技巧让DBA都惊叹
开发语言·数据库·sql·编辑器·深度优先·宽度优先
HuDie3402 小时前
prompt模版
数据库·prompt
梦想画家3 小时前
PostgreSQL 图计算双雄:Apache AGE 与 pgGraphBLAS 的融合实战指南
数据库·postgresql·图算法
逻辑驱动的ken3 小时前
Java高频面试考点场景题23
java·开发语言·数据库·面试·职场和发展·哈希算法
Francek Chen4 小时前
【大数据存储与管理】实验3:熟悉常用的HBase操作
大数据·数据库·分布式·hbase
ffqws_4 小时前
Spring @Transactional 注解详解:从入门到避坑
java·数据库·后端·spring
努力努力再努力wz4 小时前
【MySQL 进阶系列】C/C++ 如何通过客户端库访问 MySQL?从连接原理到 API 调用流程详解(附完整demo代码)
服务器·c语言·数据结构·数据库·c++·b树·mysql
七夜zippoe5 小时前
DolphinDB分布式表:创建与管理
数据库·分布式·维度·dolphindb·数据写入
何中应5 小时前
Redis集群搭建
数据库·redis·缓存