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 的假设
时钟漂移可以很大(NTP配置错误、虚拟机暂停)通常很小(NTP 正常工作)
攻击者假设恶意攻击者可以操纵时钟假设运维环境可信
故障模型更严格(考虑拜占庭故障)更宽松(只考虑崩溃故障)

从学术角度,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% 可靠的

六、谁更正确?

读完双方的论点,我认为这是一个"适用场景"的问题:

Kleppmann 的批评适用的场景
时钟漂移攻击需要拜占庭容错、对抗恶意攻击者的场景
进程暂停问题长临界区(分钟级别)、高 GC 压力的系统
不满足线性一致性需要可线性化语义的关键业务(如金融交易)
Antirez 的反驳适用的场景
时钟漂移可控运维规范的内部系统,NTP 正常工作
进程暂停是共性问题所有分布式锁场景,这不是 RedLock 的特殊问题
性能与安全的平衡对性能敏感,愿意接受"尽力而为"语义的场景

:::details 📖 点击展开:RedLock 适用性快速判断

用以下几个问题判断你的场景是否适合 RedLock:

  1. 业务能否接受锁失败?

    • 能(扣库存降级、异步任务重试)→ 可以用 Redis 锁
    • 不能(金融交易、强一致写)→ 用 etcd/ZooKeeper
  2. 锁持有时间有多长?

    • < 10 秒→ Redis 锁 + 看门狗 OK
    • 10 秒 ~ 1 分钟→ 需要仔细配置 TTL + 续期
    • > 1 分钟→ 考虑 ZooKeeper 或 etcd
  3. 运维能力如何?

    • 能维护 5 个独立 Redis 节点 + 监控 + 故障转移 → RedLock
    • 只有单 Redis 实例 → 单节点 Redis 锁或 Redisson 客户端自动切换
  4. 需要锁的场景有多关键?

    • 非核心流程(缓存更新、任务调度)→ Redis 锁够用
    • 核心流程(支付、库存)→ 需要 etcd/ZooKeeper 或 RedLock + 业务幂等 :::

七、工程实践建议

综合两方观点,以下是我的工程实践建议:

// 1. 能不用分布式锁就不用
// 很多"分布式锁"的场景其实可以用其他方案替代:
// - 幂等设计(消除重复操作)
// - 消息队列(消除并发竞争)
// - 数据库唯一约束(防止重复插入)

// 2. 必须用锁时,按这个顺序选型:
// 扣库存高性能:Redis 原子操作 + 乐观锁
// 跨服务协调:Redis SETNX + 看门狗
// 强一致要求:etcd / ZooKeeper
// 金融级可靠:etcd + 业务幂等

// 3. 使用 RedLock 时的额外注意事项:
// - 部署 5 个独立的 Redis 节点(不同机器、不同网络)
// - 开启 Redis 持久化(RDB + AOF),防止数据丢失
// - 业务层实现幂等性,兜底锁失败的情况
// - 监控 RedLock 的获取失败率,超过 1% 立即告警

八、工程代价评估

维度评估
开发成本高(5节点部署 + 运维规范)
运维成本极高(5倍于单节点 Redis)
排障复杂度高(时钟漂移、进程暂停问题难以复现)
正确性上限中(尽力而为,不保证线性一致性)
性价比低(对于大多数业务,运维成本远大于收益)

RedLock 的争议告诉我们一个道理:没有银弹。分布式锁的每一个设计决策都涉及 trade-off——性能 vs 正确性、简单性 vs 可靠性、运维成本 vs 业务收益。

下篇文章我们看 ZooKeeper 分布式锁——换一个完全不同的技术栈,从共识算法出发实现分布式锁。