🔒 Redis 分布式锁

设计原理 · 实现方案 · 常见问题 · 完整图解

🖥️ Client A 申请加锁 🖥️ Client B 申请加锁 🗄️ Redis lock_key = clientA_id TTL = 10s SET NX → 成功 ✅ SET NX → 失败 ❌ 互斥:同一时刻只有一个 客户端能持有锁 安全:持锁期间其他 客户端无法操作资源

为什么需要分布式锁

在单体应用中,我们用 ReentrantLocksynchronized 就能搞定并发问题。但在分布式系统中,多个实例同时访问共享资源时,JVM 级别的锁就失效了——因为它们根本不在同一个 JVM 里。

JVM 实例 1 Thread-1 Thread-2 synchronized 只能管这里 JVM 实例 2 Thread-3 Thread-4 JVM 实例 3 Thread-5 Thread-6 💾 共享资源(数据库 / 接口) ⚠️ 并发冲突 ✕ 互不可见 ✕ 互不可见

核心矛盾

每个 JVM 实例的锁只能控制自己内部的线程,无法约束其他实例的线程。当多个实例同时操作同一份数据(如库存扣减、订单创建),就会产生 竞态条件

分布式锁的本质:引入一个所有实例都信任的第三方(如 Redis)来协调互斥访问。

基础实现:SET NX EX

最简单的 Redis 分布式锁只需一条命令:

加锁命令

SET lock_key unique_value NX EX 30
  • NX(Not eXists):只有 key 不存在时才设置——保证互斥
  • EX(EXpire):设置过期时间——防止死锁
  • unique_value:唯一标识(如 UUID + 线程ID)——防止误删
时间 → Client A Client B ① SET NX EX ✅ 加锁成功,持有锁执行业务... ② SET NX ❌ key 已存在 等待… ③ 重试成功 获得锁 ✅ ④ DEL / 过期

释放锁(Lua 脚本保证原子性)

-- 只有自己持锁时才能释放,防止误删别人的锁
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

这里的 ARGV[1] 就是加锁时的 unique_value,通过 Lua 脚本的原子性保证「比较 + 删除」不会被打断。

实现方案的演进过程

从最简陋到最完善,Redis 分布式锁经历了 4 个阶段的演进:

V1 最简实现(有缺陷) SETNX + EXPIRE 分两步执行 → 非原子操作,可能死锁 ❌ 不推荐 V2 SET NX EX(改善) 一条命令同时完成加锁 + 过期 → 原子性解决了,但仍可能误删 ⚠️ 基本可用 V3 加唯一值 + Lua 释放 unique_value 标识持锁者 Lua 脚本原子性释放,防误删 ✅ 单节点推荐 V4 RedLock(多节点) 多个独立 Redis 节点共同裁决 → 解决主从切换导致的安全问题 ✅ 高可用推荐 🏆 生产首选 Redisson — 开箱即用的分布式锁框架 • V3 + V4 的所有能力 + 自动续期(Watchdog) • 可重入锁、公平锁、读写锁、红锁、联锁 • 自动处理锁续期、锁重入、锁释放 ✅ 实际项目首选

问题一:加锁的原子性

❌ 错误做法:SETNX + EXPIRE 分两步

如果程序在 SETNX 成功后、EXPIRE 执行前崩溃,锁将永远不会过期 → 死锁

时间 → Client ① SETNX 成功 ✅ key 写入 Redis ⚠️ 崩溃!EXPIRE 未执行 ② EXPIRE 未到达 💀 死锁!key 永不过期 所有其他客户端永远拿不到锁 ✅ SET NX EX 合并
✅ 正确做法

使用 SET key value NX EX seconds 一条命令完成加锁和设置过期,Redis 保证单条命令的原子性。

问题二:释放锁的误删(超时竞态)

客户端 A 的锁过期了,但 A 还在执行业务。此时 B 获得锁,A 执行完后把 B 的锁删了——灾难!

时间 → Client A Client B ① 加锁 执行业务逻辑中... A 的锁(TTL=10s) ② 锁过期! key 自动删除 ③ B 加锁 B 执行业务中... B 的锁 ④ A 业务执行完 DEL lock_key 💀 删了 B 的锁! 安全漏洞! ✅ 解决方案 1. 加锁存唯一ID 2. 释放前先比较 3. Lua脚本保证原子

解决方案:唯一标识 + Lua 脚本

1
加锁时存入唯一值:如 UUID:threadId
2
释放时先用 GET 比较,值匹配才 DEL
3
比较 + 删除放在同一个 Lua 脚本中,确保原子性

问题三:业务未执行完,锁就过期了

设置了 TTL=10s,但业务执行需要 30s,怎么办?锁过期后其他客户端拿到锁,并发安全被打破。

时间 → Client A 业务执行中(需要 30s) TTL 第1段 10s 🐕 续期! TTL 重置为 30s TTL 第2段 10s(续期后) 🐕 续期! TTL 第3段 业务完成 释放锁 DEL 🐕 Watchdog 机制(Redisson 实现) 加锁成功后启动后台线程,每隔 TTL/3 时间检查一次 如果持锁者还存活 → 自动续期。持锁者宕机 → 停止续期,锁自然过期

Watchdog 工作原理

1
加锁时默认 TTL = 30s,如果不指定 leaseTime
2
后台定时任务每 10s(TTL/3)检查锁是否还被当前线程持有
3
如果还持有 → 重置 TTL 为 30s;如果客户端宕机 → 定时任务终止 → 锁到期自动释放
注意

如果调用 lock(10, TimeUnit.SECONDS) 显式指定了 leaseTime,Watchdog 不会启动,到期直接释放。

问题四:GC 停顿 / 时钟漂移

即使锁的 TTL 足够长,Java 的 Full GC 停顿可能导致锁实际持有时间远超 TTL。更严重的是分布式环境下的时钟不同步问题。

时间 → Client Redis 加锁 TTL=10s SET NX EX 10 🐢 Full GC 停顿 8s! 线程 STW,无法感知任何事 TTL 10s 倒计时... 锁过期! key 已删除 GC 结束 以为自己还持有锁 操作共享资源 💥 ⚠️ 此时可能已有其他 客户端持有锁 → 并发违规 ✅ 应对策略 1. TTL 设置足够大的余量 2. Redisson Watchdog 自动续期 3. 业务逻辑加幂等性保护
💡 GC 停顿是所有分布式锁的共有挑战

不仅仅是 Redis 锁,Zookeeper 锁、etcd 锁都可能受到 GC 停顿的影响。Martin Kleppmann 在著名的 "How to do distributed locking" 文章中正是用这个论点质疑了 RedLock 的安全性。核心应对策略是:Lock 续期 + 业务幂等

问题五:主从切换导致锁丢失

这是 Redis 分布式锁最核心的可靠性问题。当 Master 持有锁后宕机,数据还没同步到 Slave,Slave 晋升为新 Master 后锁就丢了。

阶段 1:正常状态 Client A Master Slave 异步复制 🔑 lock_key = A (可能还没同步) 阶段 2:Master 宕机 Client A 💀 宕机! 新 Master ⚠️ 锁数据丢失! 阶段 3:Client B 获得锁 Client A 以为自己还 持有锁... Client B 新 Master 🔑 lock_key = B 💥 A 和 B 同时认为自己持有锁! 互斥性被打破 → 严重安全问题

RedLock:多节点解决方案

RedLock 通过在多个独立 Redis 节点上同时加锁来解决主从切换问题。客户端只有在多数节点(N/2 + 1)加锁成功才算成功。

Client Redis Node 1 ✅ 加锁成功 Redis Node 2 ✅ 加锁成功 Node 3 ✅ 成功 Node 4 ❌ 失败 Redis Node 5 ✅ 加锁成功 4/5 节点成功 ≥ N/2 + 1 = 3 ✅ 加锁成功! RedLock 关键点 • 5 个完全独立的 Redis 节点 • 节点间没有主从关系 • 所有操作在单个节点上原子执行 • 总耗时远小于锁的有效时间
⚡ RedLock 争议

RedLock 由 Redis 作者 Antirez 提出,但被 Martin Kleppmann(DDIA 作者)发文质疑。核心争论在于:

  • GC 停顿:持有锁的客户端长时间 STW,锁过期后其他客户端拿到锁,互斥被打破
  • 时钟跳跃:Redis 的过期依赖时钟,NTP 时钟回拨可能导致锁提前过期
  • RedLock 的应对:加锁时计算实际经过的时间,验证有效性

实际生产中,大多数场景用 Redisson 单节点 + Watchdog 就够了。只有极高可靠性要求的场景才需要 RedLock。

方案对比与总结

方案 原子性 防误删 防死锁 主从安全 复杂度
SETNX + EXPIRE ⚠️
SET NX EX
SET NX EX + Lua释放
Redisson(单节点) ⚠️ 中(框架封装)
RedLock(多节点)

生产环境推荐

🏆 首选:Redisson

直接用 RLock lock = redisson.getLock("lock_key"),框架帮你处理了原子性、误删、续期、可重入等所有问题。简单可靠,开箱即用。

极端场景:RedLock

如果你的业务对锁的安全性要求极高(如金融交易),且无法容忍主从切换导致的极小概率安全问题,再考虑 RedLock。

终极方案:Zookeeper / etcd

如果 Redis 的 CP 弱一致性本身就是问题,考虑使用 Zookeeper 或 etcd 这类 CP 系统来实现分布式锁,它们通过强一致性的协议(ZAB / Raft)天然保证锁的安全。

分布式锁核心问题全景 分布式锁 核心问题 ① 原子性 ② 误删问题 ③ 锁过期续期 ④ GC/时钟漂移 ⑤ 主从切换