一、演进之路:从SETNX到Redisson
分布式锁的实现并非一蹴而就,它经历了一个由简入繁、由脆弱到健壮的过程。
-
青铜时代:SETNX + EXPIRE
早期的做法是利用
SETNX(SET if Not eXists) 命令。bashSETNX lock_key client_id EXPIRE lock_key 30致命缺陷 :这两条命令不具备原子性。如果
SETNX成功但服务在EXPIRE执行前宕机,锁将永远无法释放(死锁)。 -
白银时代:原子性SET命令
Redis 2.6.12 之后,
SET命令增加了NX、PX等参数,解决了原子性问题。bashSET lock_key client_id NX PX 30000NX:仅当键不存在时设置。PX:设置毫秒级过期时间。
遗留问题:如果业务执行时间超过30秒,锁会被自动释放,导致其他客户端获取锁,引发并发冲突。此外,如果客户端A持有锁期间宕机,锁虽然会自动释放,但无法感知锁是否被"误删"(例如A恢复后删除了B持有的锁)。
-
黄金时代:Redisson与看门狗
为了解决"业务未执行完锁过期"的问题,业界引入了Redisson 。其核心在于WatchDog(看门狗)机制。
WatchDog原理 :
当客户端获取锁时,如果不指定租约时间,Redisson会默认设置30秒过期时间,并启动一个后台定时任务(WatchDog)。每隔10秒,它会检查客户端是否还持有锁,如果持有,则重置过期时间为30秒。
流程图解 :

二、至暗时刻:时钟跳跃与GC停顿
在生产环境中,理论上的完美锁往往不堪一击。
-
GC停顿(Stop-The-World)
假设客户端A获取了锁,但在执行关键业务逻辑时,JVM发生了长时间的Full GC(例如暂停了35秒)。此时,Redis中的锁(30秒过期)已经自动释放。客户端B顺利获取了锁并开始操作。当A的GC结束时,它并不知道自己已经"掉线",继续操作共享资源,导致并发冲突。
解决方案 :虽然WatchDog能缓解一部分问题,但根本解决需要依赖Fencing Token(围栏令牌) 机制,即在资源服务端检查操作请求的顺序,拒绝旧版本的请求。 -
时钟跳跃(Clock Drift)
Redis服务器和客户端的时间可能不同步。如果Redis服务器时间跳变(例如NTP校准导致时间向前跳跃),锁的剩余生存时间可能会意外缩短,导致锁提前释放。
三、终极方案之争:Redlock算法与争议
为了解决单点故障(主从切换导致锁丢失)的问题,Redis作者antirez提出了Redlock算法。
Redlock核心步骤:
- 获取当前时间(毫秒)。
- 依次向N个(通常5个)独立的Redis节点发送加锁请求(设置极短的超时时间)。
- 计算获取锁的耗时。
- 如果成功加锁的节点数 > N/2 且 耗时 < 锁有效期,则加锁成功。
- 如果失败,向所有节点发送释放锁请求。
流程图解 :

Martin Kleppmann的质疑 :
Martin指出,Redlock依赖系统时钟的稳定性。如果发生网络分区,或者某个节点因为GC停顿导致响应变慢,可能会破坏算法的安全性。此外,如果没有Fencing Token,Redlock也不能保证强一致性。
四、架构选型:Redis vs ZooKeeper
| 特性 | Redis分布式锁 | ZooKeeper分布式锁 |
|---|---|---|
| 一致性模型 | AP(最终一致性,存在主从切换丢锁风险) | CP(强一致性,基于ZAB协议) |
| 实现复杂度 | 低(尤其是使用Redisson) | 高(需要维护ZK集群,API较繁琐) |
| 性能 | 极高(内存操作,微秒级) | 较高(但在高并发写场景下不如Redis) |
| 可靠性 | 中等(受GC、时钟影响) | 极高(临时顺序节点天然防死锁) |
| 适用场景 | 高并发、对锁可靠性要求非绝对严苛(如缓存击穿、普通秒杀) | 金融级交易、配置管理、对一致性要求极高 |
五、常见面试题
Q1: Redis分布式锁的"误删"问题是如何产生的?如何解决?
A: 产生原因:客户端A获取锁,因GC卡顿导致业务执行时间超过锁的TTL,锁自动释放。客户端B获取锁。A恢复后执行删除操作,误删了B的锁。
解决: 必须使用Lua脚本保证"判断Value是否属于自己"和"删除"的原子性。即:if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end。
Q2: Redlock算法在什么情况下会失效?
A: 1. 网络延迟导致大部分节点加锁超时;2. 节点发生时钟跳跃(向后或向前);3. 客户端发生长时间GC停顿,导致锁在持有期间过期(这是所有基于TTL锁的通病)。
Q3: 如何设计一个可重入的分布式锁?
A: 利用Hash结构。Key为锁名,Field为
UUID:ThreadId,Value为重入次数。加锁时,如果Field存在且匹配,则Value+1;否则执行SETNX。解锁时Value-1,为0时删除Key。