🔒 Redis 分布式锁
设计原理 · 实现方案 · 常见问题 · 完整图解
为什么需要分布式锁
在单体应用中,我们用 ReentrantLock、synchronized 就能搞定并发问题。但在分布式系统中,多个实例同时访问共享资源时,JVM 级别的锁就失效了——因为它们根本不在同一个 JVM 里。
核心矛盾
每个 JVM 实例的锁只能控制自己内部的线程,无法约束其他实例的线程。当多个实例同时操作同一份数据(如库存扣减、订单创建),就会产生 竞态条件。
分布式锁的本质:引入一个所有实例都信任的第三方(如 Redis)来协调互斥访问。
基础实现:SET NX EX
最简单的 Redis 分布式锁只需一条命令:
加锁命令
SET lock_key unique_value NX EX 30
- NX(Not eXists):只有 key 不存在时才设置——保证互斥
- EX(EXpire):设置过期时间——防止死锁
- unique_value:唯一标识(如 UUID + 线程ID)——防止误删
释放锁(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 个阶段的演进:
问题一:加锁的原子性
如果程序在 SETNX 成功后、EXPIRE 执行前崩溃,锁将永远不会过期 → 死锁。
使用 SET key value NX EX seconds 一条命令完成加锁和设置过期,Redis 保证单条命令的原子性。
问题二:释放锁的误删(超时竞态)
客户端 A 的锁过期了,但 A 还在执行业务。此时 B 获得锁,A 执行完后把 B 的锁删了——灾难!
解决方案:唯一标识 + Lua 脚本
UUID:threadId问题三:业务未执行完,锁就过期了
设置了 TTL=10s,但业务执行需要 30s,怎么办?锁过期后其他客户端拿到锁,并发安全被打破。
Watchdog 工作原理
如果调用 lock(10, TimeUnit.SECONDS) 显式指定了 leaseTime,Watchdog 不会启动,到期直接释放。
问题四:GC 停顿 / 时钟漂移
即使锁的 TTL 足够长,Java 的 Full GC 停顿可能导致锁实际持有时间远超 TTL。更严重的是分布式环境下的时钟不同步问题。
不仅仅是 Redis 锁,Zookeeper 锁、etcd 锁都可能受到 GC 停顿的影响。Martin Kleppmann 在著名的 "How to do distributed locking" 文章中正是用这个论点质疑了 RedLock 的安全性。核心应对策略是:Lock 续期 + 业务幂等。
问题五:主从切换导致锁丢失
这是 Redis 分布式锁最核心的可靠性问题。当 Master 持有锁后宕机,数据还没同步到 Slave,Slave 晋升为新 Master 后锁就丢了。
RedLock:多节点解决方案
RedLock 通过在多个独立 Redis 节点上同时加锁来解决主从切换问题。客户端只有在多数节点(N/2 + 1)加锁成功才算成功。
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(多节点) | ✅ | ✅ | ✅ | ✅ | 高 |
生产环境推荐
直接用 RLock lock = redisson.getLock("lock_key"),框架帮你处理了原子性、误删、续期、可重入等所有问题。简单可靠,开箱即用。
如果你的业务对锁的安全性要求极高(如金融交易),且无法容忍主从切换导致的极小概率安全问题,再考虑 RedLock。
如果 Redis 的 CP 弱一致性本身就是问题,考虑使用 Zookeeper 或 etcd 这类 CP 系统来实现分布式锁,它们通过强一致性的协议(ZAB / Raft)天然保证锁的安全。