跳至主要內容

程序员小富大约 5 分钟

大家好,我是小富。

《十万个why》系列持续更新中

做分布式系统的人大概都遇到过这个问题:Redis 做分布式锁,单实例有单点故障风险。Redis 的作者 Antirez 专门设计了 RedLock 算法来解决这个问题,向多个独立的 Redis 实例同时加锁,多数派成功才算拿到锁,听起来很靠谱。

但如果你最近看过 Redisson 的源码或文档,会发现 RedLock 相关的 API 已经被标记为废弃了。一个由 Redis 作者亲自设计的分布式锁算法,为什么会被最主流的 Redis Java 客户端给废弃?

RedLock 算法的核心思路

先简单回顾一下 RedLock 的流程:

  1. 获取当前时间(毫秒级)
  2. 依次向 N 个独立的 Redis 实例发送加锁请求(SET key value NX PX timeout)
  3. 如果在大多数实例(N/2 + 1)上加锁成功,并且总耗时没超过锁的过期时间,则认为加锁成功
  4. 如果加锁失败,向所有实例发送解锁请求

假设有 5 个 Redis 实例,只要在 3 个以上加锁成功就 OK。即使其中 1-2 个实例宕机了,锁依然有效。

看起来很完美,对吧?

Martin Kleppmann 的致命一击

2016 年,分布式系统领域的大牛 Martin Kleppmann(《数据密集型应用系统设计》的作者)专门写了一篇文章《How to do distributed locking》来分析 RedLock 的问题。他提出了一个非常经典的反例:

GC 停顿导致锁失效

Client 1: 加锁成功,锁有效期 30 秒
Client 1: 发生了一次 Full GC,STW 停顿了 35 秒
Client 1: GC 结束,它以为自己还持有锁
Client 1: 执行业务操作...

但实际上锁早就过期了

Client 2: 在 Client 1 GC 期间,成功获取了同一把锁
Client 2: 也在执行业务操作...

两个客户端同时持有"锁",互斥被打破了

这不是 RedLock 特有的问题,任何基于过期时间的分布式锁都有这个风险。但 Kleppmann 的观点是:既然 RedLock 也解决不了这个根本问题,那它比单实例加锁的额外复杂度就毫无价值。

时钟漂移问题

RedLock 算法有一个隐含的前提:所有节点的时钟走速大致相同。

但现实中,服务器的系统时钟并不总是可靠的:

  • NTP 校时可能导致时间跳变
  • 虚拟机环境下时钟可能不稳定
  • 闰秒处理不当也会导致时间异常

考虑这个场景:

Redis 实例 A、B、C、D、E

1. Client 获取锁,在 A、B、C 上成功(3/5 多数派),锁有效期 30 秒
2. 实例 C 的系统时钟突然向前跳了 31 秒(NTP 校时)
3. 实例 C 上的锁因为"过期"被释放
4. 另一个 Client 获取锁,在 C、D、E 上成功(也是 3/5 多数派)
5. 两个 Client 同时持有锁

Antirez 反驳说这种时钟跳变可以通过运维手段避免,但 Kleppmann 认为分布式锁的正确性不应该依赖运维。

Antirez 的反击

Redis 的作者 Antirez 也不甘示弱,写了一篇回应文章来反驳。他的核心论点是:

  1. GC 停顿问题:可以在执行业务操作前再检查一次锁是否还有效(检查剩余时间)
  2. 时钟问题:合理配置 NTP,使用 adjtime 而不是 settimeofday,时钟跳变不应该发生
  3. RedLock 在现实场景中已经足够安全

但 Kleppmann 的反驳也很直接:

如果你需要一个在任何情况下都正确的分布式锁,用 ZooKeeper 或 etcd 这种基于共识算法的系统,它们通过 Fencing Token 机制来保证绝对的互斥性。如果你只是需要一个"尽最大努力"的锁来避免重复计算,那单实例 Redis 加锁就够了,没必要搞 RedLock 这么复杂。

这就是著名的"RedLock 定位尴尬"论:

  • 要求严格正确性?RedLock 做不到,用 ZooKeeper
  • 只要尽力而为?单实例 Redis 就够了,不需要 RedLock

RedLock 卡在中间,复杂度高,但正确性不够强,性价比很低。

Fencing Token:真正靠谱的方案

Kleppmann 提出的替代方案是 Fencing Token(防护令牌):

1. 客户端从锁服务获取锁,同时拿到一个单调递增的 Token(比如 34)
2. 客户端带着 Token 34 去操作存储服务
3. 存储服务记录见过的最大 Token
4. 如果另一个客户端拿着 Token 33(旧锁)来操作,存储服务拒绝它

ZooKeeper 的 zxid 天然就是一个单调递增的 Fencing Token。而 Redis 没有内建这种机制,需要额外实现,这又增加了复杂度。

Redisson 为什么决定废弃

Redisson 废弃 RedLock 的直接原因是上述争议始终没有定论,RedLock 的正确性在理论上确实存在缺陷。与其让用户误以为 RedLock 能提供"绝对安全"的分布式锁,不如废弃它,引导用户选择更合适的方案。

Redisson 的替代方案是:普通的 Redis 锁 + Wait 同步机制。当主节点加锁成功后,会等待数据同步到从节点(通过 WAIT 命令),在一定程度上降低了主从切换导致锁丢失的风险。虽然这也不是理论完美的方案,但在实际生产中已经足够可靠,而且比 RedLock 简单得多。

那实际项目中该怎么选?

场景推荐方案
效率型加锁(重复执行不会造成严重后果)Redis 单实例/主从 + Redisson 普通锁
正确性型加锁(涉及资金、库存、数据一致性)ZooKeeper / etcd + Fencing Token
已经在用 Redis,不想引入新组件Redisson 普通锁 + WAIT + 业务层幂等兜底

说到底,分布式锁没有银弹。RedLock 被废弃不是因为它的设计思路有问题,而是因为它试图用一个本质上不具备共识能力的系统(Redis)去做需要共识保证的事情。定位不清晰,自然就尴尬了。


我是小富,下期见。

上次编辑于: