[Redis小技巧29]从 Setnx 到 Redlock:Redis 分布式锁的演进之路与生产级实践

在单机系统中,我们习惯使用 synchronizedReentrantLock 来保证线程安全。然而,在微服务架构普及的今天,服务实例往往部署在多台机器上,JVM(Java Virtual Machine,Java 虚拟机) 级别的锁无法跨进程生效。当多个客户端同时竞争共享资源(如秒杀库存、数据库行记录、定时任务调度)时,分布式锁便应运而生。

Redis 凭借其高性能、原子操作特性以及天然的支持过期时间(TTL),成为了实现分布式锁的首选中间件。

一、初识分布式锁------SETNX + EXPIRE

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

原理:

利用 SETNX 的互斥性。当 Key 不存在时,设置成功,代表获取锁;当 Key 已存在时,设置失败,代表锁被占用。为了防止客户端宕机导致锁无法释放,必须配合 EXPIRE 设置过期时间。

伪代码逻辑:

java 复制代码
if (jedis.setnx(lock_key, "1") == 1) {
    jedis.expire(lock_key, 10); // 设置10秒过期
    try {
        doSomething();
    } finally {
        jedis.del(lock_key);
    }
}

存在的问题:

  • 原子性缺失: SETNXEXPIRE 是两条独立的指令。如果客户端在 SETNX 成功后、EXPIRE 执行前崩溃(如进程被杀、断电),这把锁将永远无法释放(死锁),导致后续所有请求阻塞。

二、原子性优化------Lua 脚本与 SET 扩展指令

为了解决 SETNXEXPIRE 非原子执行的问题,业界提出了两种优化方案。

方案 A:Lua 脚本

利用 Redis 执行 Lua 脚本的原子性,将加锁和设置过期时间封装在一个脚本中。

方案 B:SET 扩展参数(推荐)

Redis 2.6.12 版本后,SET 命令支持了扩展参数,能够原子地完成"不存在则设置"和"设置过期时间"两个动作。

命令格式:

bash 复制代码
SET key value [EX seconds] [PX milliseconds] [NX]

参数解析:

  • NX: Not Exists,仅当键不存在时执行。
  • EX/PX: 设置过期时间(秒/毫秒)。

演进对比表:

方案 原子性 复杂度 推荐指数 备注
SETNX + EXPIRE 存在死锁风险,生产环境禁用的基础版
Lua 脚本 ⭐⭐⭐ 灵活,但代码维护成本稍高
SET ... NX PX ⭐⭐⭐⭐⭐ 官方推荐,简洁高效

三、安全性与误删问题------唯一标识与 Lua 解锁

在第二阶段的基础上,解决了加锁的原子性,但解锁环节依然存在隐患。

场景描述:

客户端 A 获取了锁,设置了 10 秒过期。由于业务逻辑卡顿(如 Full GC),执行了 15 秒。

  1. 第 10 秒时,锁自动过期释放。
  2. 客户端 B 成功获取锁。
  3. 第 15 秒时,客户端 A 执行完毕,执行 DEL 命令释放锁。
  4. 结果: 客户端 A 误删了客户端 B 的锁!

解决方案:

  • Value 唯一化: 在加锁时,Value 设置为一个唯一标识(如 UUID + ThreadID)。
  • 解锁校验: 删除锁之前,先判断 Value 是否匹配。
  • 原子性保障: "判断 Value" 和 "删除 Key" 必须原子执行,因此必须使用 Lua 脚本。

解锁 Lua 脚本:

lua 复制代码
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

四、高可用挑战------Redlock 算法

上述方案在 Redis 单机或"主从复制"架构下仍存在缺陷。如果 Redis Master 节点在获取锁后、同步给 Slave 之前宕机,Slave 晋升为新 Master,锁信息丢失,导致多个客户端同时持有锁。

为了解决这个问题,Redis 作者提出了 Redlock 算法。

核心思想:

部署 N 个(通常 5 个)独立的 Redis Master 节点。获取锁时,客户端需向所有节点发起请求。

执行流程:

  1. 获取当前时间(毫秒)。
  2. 向 N 个节点依次发送加锁请求(设置较短的超时时间)。
  3. 计算获取锁的耗时。
  4. 成功条件: 客户端在至少 N/2 + 1 个节点上获取锁成功,且总耗时小于锁的有效期。

Redlock 的争议:

尽管 Redlock 提高了安全性,但也存在争议(如 Martin Kleppmann 提出的时钟跳跃问题)。在实际工程中,如果不需要极致的强一致性,通常使用"单机 Redis + 主从复制 + 等待从库同步"或 Zookeeper/Etcd 等 CP 系统作为替代。

五、常见面试题与解析

Q1: 既然有了 SETNX,为什么还需要 Redlock?

A: SETNX 方案通常基于单点 Redis。如果 Redis Master 宕机且数据未同步到 Slave,会导致锁丢失,破坏互斥性。Redlock 通过多数派原则(Quorum)解决了单点故障带来的数据一致性问题,适用于对锁安全性要求极高的场景。

Q2: 业务执行时间超过了锁的过期时间怎么办?

A: 这是一个经典问题,通常有三种解法:

  1. 调大过期时间: 预估业务最大耗时,但这会导致资源浪费。
  2. 看门狗机制: 类似 Redisson 的实现,启动一个后台线程,每隔一段时间(如 10s)检测锁是否还持有,如果持有则自动续期(Reset TTL)。
  3. 快速失败: 如果获取锁失败或执行超时,直接抛出异常,不进行重试。

Q3: Redis 分布式锁和 Zookeeper 分布式锁有什么区别?

A:

  • Redis: 基于 AP (Availability + Partition Tolerance)模型(通常),性能极高,适合高并发、对锁安全性要求不是绝对严苛的场景(如缓存重建、普通秒杀)。
  • Zookeeper: 基于 CP (Consistency + Partition Tolerance)模型,利用临时顺序节点实现,强一致性,但性能略低于 Redis,适合对数据一致性要求极高的场景(如 Leader 选举、配置管理)。

CP (Consistency + Partition Tolerance):"宁可宕机,不可出错"(严谨派)

AP (Availability + Partition Tolerance):"宁可数据旧点,也要一直服务"(实用派)

相关推荐
yume_sibai16 分钟前
06-Rust Web 开发实战(Axum 框架 + 数据库 + JWT 认证 + 中间件 + 部署)
前端·数据库·rust
AugustRed1 小时前
Neo4j 图数据库原理 + 应用场景简单介绍
数据库·neo4j
Gl�ria2 小时前
Redis 高可用架构对比
数据库·redis
IpdataCloud2 小时前
高可用的IP数据接口平台有哪些?从可用性、P99延迟到离线部署的评估框架(含代码)
数据库·tcp/ip·ip
呆萌很2 小时前
MySQL 数据库和表的管理操作(命令行)
数据库·mysql
IvorySQL3 小时前
当PostgreSQL“听懂”MySQL——协议兼容层的设计与实战
数据库·人工智能·postgresql
这个DBA有点耶4 小时前
银行核心系统数据库迁移怎么选?6 步法+5 个避坑指南
数据库·安全·架构
Zenova EdgeOS4 小时前
Python 工业边缘 redis-py 异步实战
开发语言·redis·python
风哥2号4 小时前
数据库教程FGMT31‑Linux平台MySQL9.7安装配置与版本升级
数据库
hanchenxing4 小时前
前端异步任务三方案:Celery vs Redis Stream vs BullMQ 实战对比消息队列
前端·数据库·redis·异步任务·方案对比