在分布式系统中,多个进程或服务实例常常需要互斥地访问共享资源。Redis 凭借其高性能和原子命令,成为实现分布式锁的热门选择。然而,从简单的 SETNX 到 Redlock 算法,再到社区对其安全性的激烈争论,Redis 分布式锁的演进充满了工程上的权衡。本文将从面试题的角度,梳理 Redis 分布式锁的实现原理,并深入探讨 Redlock 争议的核心。
一、为什么需要分布式锁?
在单机环境中,我们可以用 synchronized 或 ReentrantLock 保证线程安全。但在分布式部署下,多个 JVM 进程之间无法共享这些锁。分布式锁需要满足以下基本要求:
- 互斥性:任意时刻只有一个客户端能持有锁。
- 避免死锁:即使持有锁的客户端崩溃,锁最终也能被释放。
- 容错性:只要大部分 Redis 节点可用,锁服务就能正常工作。
- 可重入性(可选):同一客户端可以多次获取同一把锁。
二、基于 Redis 的分布式锁实现
1. 基础版本:SETNX + EXPIRE
最直观的做法是使用 SETNX(SET if Not eXists)命令:
SETNX lock_key unique_value
EXPIRE lock_key 10
如果 SETNX 返回 1,表示获取锁成功;否则失败。释放锁时删除 key。
问题:SETNX 和 EXPIRE 是两条命令,不是原子操作。如果客户端在 SETNX 成功后、EXPIRE 执行前崩溃,锁将永远不会过期,导致死锁。
2. 改进版本:SET 扩展命令
Redis 2.6.12 之后,SET 命令支持 NX 和 EX 选项,将加锁变为原子操作:
SET lock_key unique_value NX EX 10
NX:仅当 key 不存在时设置。EX 10:设置 10 秒过期时间。unique_value:通常用 UUID 或客户端标识,用于安全释放锁。
释放锁时,需要先判断 value 是否为自己设置的值,再删除 key。这一步必须用 Lua 脚本保证原子性:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
3. 锁续期与看门狗
如果业务执行时间超过锁的过期时间,锁可能被其他客户端获取。Redisson 等客户端提供了“看门狗”机制:获取锁成功后,启动一个后台线程定期续期(通常为过期时间的 1/3),直到客户端主动释放锁或崩溃。
4. 单点 Redis 的局限性
以上方案基于单个 Redis 实例。如果该实例宕机,整个锁服务不可用。即使配置了主从复制,也可能出现主从切换丢锁的问题:
- 客户端 A 在主节点获取锁。
- 主节点在将锁同步到从节点之前宕机。
- 从节点升级为主节点,客户端 B 可以获取同一把锁。
此时两个客户端同时持有锁,互斥性被破坏。
三、Redlock 算法
为了解决单点问题,Redis 作者 antirez 提出了 Redlock 算法。其核心思想是:在多个独立的 Redis 节点上获取锁,只有多数节点成功,才认为加锁成功。
算法步骤
假设有 N 个独立的 Redis 节点(通常 N=5):
- 获取当前时间戳 T1。
- 依次向 N 个节点发送加锁命令,使用相同的 key 和 value,并设置较短的超时时间(如 5-50ms),避免在某个节点上阻塞过久。
- 计算获取锁的总耗时:T2 – T1。
- 加锁成功条件:
- 在超过半数(N/2 + 1)的节点上成功获取锁。
- 总耗时小于锁的有效时间。
- 如果加锁成功,锁的有效时间 = 初始有效时间 – 总耗时。
- 如果加锁失败,向所有节点发送释放锁命令。
释放锁
与单机类似,需要向所有节点发送 Lua 脚本删除 key。
Redlock 的争议
2016 年,分布式系统专家 Martin Kleppmann 发表文章《How to do distributed locking》,对 Redlock 提出了尖锐批评。随后 antirez 撰写了反驳文章。争议主要集中在以下几点:
1. 依赖系统时钟
Redlock 的安全性依赖于各节点时钟的单调性。如果某个节点的时钟发生跳跃(如 NTP 校正),可能导致锁提前过期。Kleppmann 指出,即使没有时钟跳跃,进程暂停(如 GC)也可能导致客户端在锁过期后继续操作共享资源。
2. 无法保证 fencing token
Kleppmann 认为,分布式锁的正确性不应依赖锁本身,而应依赖** fencing token**(递增的令牌)。客户端在访问共享资源时携带 token,资源端拒绝旧 token 的请求。Redlock 没有提供这种机制,因此即使锁机制本身正确,也无法防止因进程暂停导致的并发问题。
3. 性能与复杂度
Redlock 需要多个独立节点,运维成本高。而且每次加锁需要网络往返多个节点,延迟高于单机 Redis。
4. antirez 的回应
antirez 认为,Redlock 的设计目标是提供一种“足够好”的分布式锁,适用于对效率要求高、对正确性要求不是极端严格的场景。他承认时钟跳跃和进程暂停是问题,但认为这些属于极端情况,且可以通过合理运维(如禁用 NTP 跳跃)缓解。
争议的实质
这场争论的本质是对分布式锁的定位不同:
- 效率型锁:用于避免重复工作(如缓存击穿),偶尔失效可以接受。
- 正确型锁:用于保证数据一致性,必须绝对可靠。
Redlock 更适合效率型场景。对于正确型场景,Kleppmann 建议使用带有 fencing token 的锁服务,如 ZooKeeper 或 etcd。
四、面试常见问题
-
Redis 分布式锁如何避免死锁?
设置过期时间,并使用 Lua 脚本安全释放。 -
如何解决锁被其他客户端误删?
设置 unique_value,释放时校验。 -
Redlock 解决了什么问题?
解决了单点 Redis 故障导致锁服务不可用的问题。 -
Redlock 的争议点是什么?
依赖时钟、无法提供 fencing token、进程暂停导致锁失效。 -
生产环境如何选择?
如果对正确性要求极高,建议使用 ZooKeeper 或 etcd;如果追求性能且能容忍极端情况,Redis 单机 + 看门狗已足够。
五、总结
Redis 分布式锁从简单的 SETNX 演进到 Redlock,体现了分布式系统设计的复杂性。没有一种锁方案是完美的,关键在于理解业务对一致性的要求。对于大多数互联网场景,单机 Redis + 过期时间 + Lua 释放 + 看门狗续期已经足够。Redlock 提供了更高的可用性,但引入了时钟依赖和复杂度。在面试中,能够清晰阐述各种方案的优缺点及争议,往往比给出一个“标准答案”更能体现深度。
未经允许不得转载:任鹏个人博客 » Redis 分布式锁的实现原理与 Redlock 争议

