RedLock 争议与批评
一场分布式锁领域的"世纪论战"
2016年2月,Redis 作者 Antirez 发表 RedLock 算法。
2016年4月,Cambridge 教授 Martin Kleppmann(DDIA 作者)发表长文《How to do distributed locking》,逐条反驳 RedLock。
2016年4月,Antirez 发表反驳文章《On the correctness of RedLock》。
这场论战不是两个工程师在社交媒体上的口水仗,而是两篇严谨的技术分析——Kleppmann 从分布式系统理论出发,指出 RedLock 的数学证明存在漏洞;Antirez 从工程实践出发,逐一反驳 Kleppmann 的假设过于理想化。
谁的论点更有说服力?读完这篇文章,你自己判断。
一、Kleppmann 的核心批评
批评一:RedLock 不满足线性一致性
Kleppmann 的核心论点是:RedLock 不满足 CAP 定理中的 C(一致性),因此不能用于需要强一致性的场景。
CAP 定理:
- C (Consistency): 线性一致性,所有节点看到的数据是同时更新的
- A (Availability): 可用性,每个请求都能得到响应
- P (Partition Tolerance): 分区容错,网络分区时系统仍能工作
RedLock 的实际行为:
- 在正常情况下(无分区):提供可用性,但不保证线性一致
- 在分区情况下:可能同时返回"获取成功"和"获取失败"
Kleppmann 的结论:
RedLock 既不是 CP 系统,也不是 AP 系统。
它是一个"尽力而为"的系统,不能用于需要强一致保证的场景。
时钟漂移的具体场景
Kleppmann 在论文中给出了具体的攻击场景:
// 假设:
// - 5 个 Redis 节点,TTL = 10 秒
// - 节点3的时钟比实际时间快 2 秒(时钟漂移)
// - C1 在 T0 获取锁,在节点1-5 上的锁过期时间分别是:
C1 的锁过期时间:
节点1: T0 + 10s = T10 (正常)
节点2: T0 + 10s = T10 (正常)
节点3: T0 + 8s = T8 (时钟漂移,快了2秒)
节点4: T0 + 10s = T10 (正常)
节点5: T0 + 10s = T10 (正常)
// 场景演变:
T8: 节点3的锁过期
T9: C2 在节点3上获取锁成功
T10: 节点1,2,4,5的锁过期
T11: C2 在节点4上获取锁成功(2/5 < 3,不满足多数派)
此时 C2 认为自己没有拿到锁,退出。
但如果 C2 在 T9~T10 之间已经进入了临界区……
Kleppmann 的论点是:RedLock 的加锁成功判断依赖于每个节点的本地时钟,但时钟可能漂移,导致不同节点对"锁是否有效"的判断不一致。
Antirez 的反驳
Antirez 的回应是:
1. 时钟漂移在生产环境中是可以管理的
- 使用 NTP 同步,漂移通常在毫秒级别
- RedLock 对时钟漂移有事后检查(加锁后验证总耗时)
2. 5节点多数派设计已经覆盖了时钟漂移的窗口
- 2秒漂移 < TTL的40%,在可接受范围内
- 要让时钟漂移导致安全问题,需要 >40% 的节点同时漂移
3. 任何分布式系统都无法完全消除时钟问题
- 物理时钟不可靠,逻辑时钟(Lamport Clock)也有自己的问题
- RedLock 的设计已经是在性能和安全性之间的合理平衡
【架构权衡】
这场关于时钟漂移的争论,核心分歧在于系统模型假设:
从学术角度,Kleppmann 更严谨。从工程角度,Antirez 更务实。对于大多数业务系统,Antirez 的假设是合理的;但对于金融、证券等需要拜占庭容错的系统,Kleppmann 的批评值得重视。
二、批评二:进程暂停问题
Kleppmann 提出了另一个根本性问题:进程暂停(Process Pausing)。
// 进程暂停的攻击场景
// GC 暂停、虚拟化暂停、内核调度延迟
C1 获取 RedLock 成功(5/5 节点成功)
long endTime = System.nanoTime() + ttl; // 记录过期时间 T10
// ← GC 暂停发生!进程被挂起 35 秒 →
T45: C1 恢复执行
T45: C1 检查是否过期:当前时间 > endTime?YES(已过期)
T45: C1 进入临界区执行操作
T45: C2 在节点1上获取锁(因为节点1在 T10 已经过期)
此时 C1 和 C2 同时在临界区!RedLock 未能提供互斥保证。
问题在于:Java 的 System.currentTimeMillis() 和 System.nanoTime() 在 GC 暂停期间也会被"暂停"——如果用当前时间判断锁是否过期,GC 暂停期间流逝的时间不会被计入。
Antirez 的反驳
Antirez 的回应:
进程暂停是所有分布式锁的问题,不仅仅是 RedLock 的问题。
无论是 ZooKeeper、etcd 还是数据库锁,持有锁的进程都可能被暂停。
解决方案:
1. 使用实时操作系统(RTOS)—— 成本高,不适合通用场景
2. 锁持有者使用" fencing token"机制,服务器端拒绝过期请求
3. 接受风险,在应用层做好幂等性设计
RedLock 的设计与 ZooKeeper / etcd 在这一点上是等价的。
批评 RedLock 的进程暂停问题,等于批评所有分布式锁。
💡
Kleppmann 在批评中提出的"fencing token"机制是解决进程暂停问题的标准方案:
C1 获取锁时,同时获得一个递增的 token(例如基于 Redis 的原子递增)
C1 进入临界区前,将 token 发给资源服务器
资源服务器记录见过的最大 token,拒绝 token < 最大token 的请求
T0: C1 获取锁,收到 token = 5
T30: GC 暂停开始
T65: GC 结束,C1 醒来,发送 token=5 请求
T65: C2 获取锁,收到 token = 6
T65: 资源服务器看到 token=5 < maxToken=6,拒绝!
这是分布式锁正确性的标准保证机制,但需要服务器端配合实现。
三、批评三:RedLock 的解锁可能不安全
Kleppmann 指出了另一个问题:RedLock 的解锁逻辑存在竞态条件。
// RedLock 解锁步骤:
// 1. 释放锁(在所有节点上执行 DEL)
// 2. 每个节点独立验证是否成功
// 竞态场景:
T0: C1 在节点1-5上持有锁
T5: 节点1 的锁过期
T6: C2 在节点1上获取锁
T7: C1 执行解锁,在节点1-5 上发送 DEL 命令
T8: 节点1 收到 DEL,删除的是 C2 的锁!
T9: 节点2-5 收到 DEL,删除的是 C1 的锁
结果:C1 的解锁操作删除了 C2 的锁!C2 以为锁还在,但实际上已经被 C1 误删了。
Antirez 的反驳
Antirez 的反驳:
这是所有分布式锁的共同问题,不仅仅是 RedLock。
正确的做法:
1. 在 DEL 前先检查 value 是否匹配(用 Lua 脚本保证原子性)
2. 解锁操作本身返回"是否真的删除了自己的锁"
RedLock 的实现(Redisson)中,unlock 操作使用的是:
if (redis.call('get', KEYS[1]) == ARGV[1])
then redis.call('del', KEYS[1])
这个检查确保只删除自己的锁。
⚠️
Kleppmann 的批评针对的是理论上的 RedLock 设计,而 Antirez 的反驳基于Redisson 的实际实现。如果你的 RedLock 实现没有用 Lua 脚本做 value 匹配,Kleppmann 的批评完全成立。确保你的解锁代码是:
if redis.call('get', KEYS[1]) == ARGV[1] then
redis.call('del', KEYS[1])
return 1
else
return 0
end
四、批评四:RedLock 不适合分布式事务
Kleppmann 的最终结论是:RedLock 不应该被用于分布式事务场景。
分布式事务要求:
- 原子性:所有节点要么全部提交,要么全部回滚
- 一致性:跨节点的数据保持一致
- 隔离性:并发事务相互隔离
- 持久性:提交后的数据不会丢失
RedLock 能保证的:
- 互斥性:在锁持有期间保证互斥
- 有界持久性:锁数据存在于 Redis 内存中
RedLock 不能保证的:
- 无界持久性:如果 Redis 没有开启 RDB/AOF,宕机后锁数据丢失
- 线性一致性:见批评一
Kleppmann 的建议是:如果需要分布式事务,应该使用 etcd/ZooKeeper(基于 Raft/Paxos 共识),而不是 Redis 系列方案。
五、批评五:单节点 vs 多节点的权衡
// RedLock 的一个被忽视的成本:
// 5 个节点意味着 5 倍的运维复杂度和 5 倍的故障点
// 单节点 Redis 锁的可用性:99.9%
// RedLock 的可用性:P(至少3节点存活) = 99.9999999%
// 实际生产中:
// - 5 个节点意味着 5 倍的配置管理
// - 5 个节点意味着 5 倍的监控告警
// - 5 个节点意味着 5 倍的故障排查复杂度
// - 5 个节点意味着 5 倍的运维成本
// 99.9% vs 99.9999% 的差距有多大?
// 99.9%: 每年宕机 8.76 小时
// 99.9999%: 每年宕机 31.5 秒
// 对于大多数业务系统,99.9% 已经足够。
// 为了每年省下 8 小时的宕机时间,值得增加 5 倍运维成本吗?
【架构权衡】
Kleppmann 在论文最后给出了一个务实的建议:
1. 不要使用 Redis 分布式锁来保护需要强一致性的资源
2. 如果必须使用 Redis 锁,接受其"尽力而为"的语义
3. 在锁保护的临界区内部,所有操作必须是幂等的
4. 对于需要强一致性的场景,使用 etcd、ZooKeeper 或数据库
5. 永远不要假设分布式锁是 100% 可靠的
六、谁更正确?
读完双方的论点,我认为这是一个"适用场景"的问题:
:::details 📖 点击展开:RedLock 适用性快速判断
用以下几个问题判断你的场景是否适合 RedLock:
-
业务能否接受锁失败?
- 能(扣库存降级、异步任务重试)→ 可以用 Redis 锁
- 不能(金融交易、强一致写)→ 用 etcd/ZooKeeper
-
锁持有时间有多长?
< 10 秒→ Redis 锁 + 看门狗 OK
10 秒 ~ 1 分钟→ 需要仔细配置 TTL + 续期
> 1 分钟→ 考虑 ZooKeeper 或 etcd
-
运维能力如何?
- 能维护 5 个独立 Redis 节点 + 监控 + 故障转移 → RedLock
- 只有单 Redis 实例 → 单节点 Redis 锁或 Redisson 客户端自动切换
-
需要锁的场景有多关键?
- 非核心流程(缓存更新、任务调度)→ Redis 锁够用
- 核心流程(支付、库存)→ 需要 etcd/ZooKeeper 或 RedLock + 业务幂等
:::
七、工程实践建议
综合两方观点,以下是我的工程实践建议:
// 1. 能不用分布式锁就不用
// 很多"分布式锁"的场景其实可以用其他方案替代:
// - 幂等设计(消除重复操作)
// - 消息队列(消除并发竞争)
// - 数据库唯一约束(防止重复插入)
// 2. 必须用锁时,按这个顺序选型:
// 扣库存高性能:Redis 原子操作 + 乐观锁
// 跨服务协调:Redis SETNX + 看门狗
// 强一致要求:etcd / ZooKeeper
// 金融级可靠:etcd + 业务幂等
// 3. 使用 RedLock 时的额外注意事项:
// - 部署 5 个独立的 Redis 节点(不同机器、不同网络)
// - 开启 Redis 持久化(RDB + AOF),防止数据丢失
// - 业务层实现幂等性,兜底锁失败的情况
// - 监控 RedLock 的获取失败率,超过 1% 立即告警
八、工程代价评估
RedLock 的争议告诉我们一个道理:没有银弹。分布式锁的每一个设计决策都涉及 trade-off——性能 vs 正确性、简单性 vs 可靠性、运维成本 vs 业务收益。
下篇文章我们看 ZooKeeper 分布式锁——换一个完全不同的技术栈,从共识算法出发实现分布式锁。