大家好,我是小富。
《十万个why》系列持续更新中
做分布式系统的人大概都遇到过这个问题:Redis 做分布式锁,单实例有单点故障风险。Redis 的作者 Antirez 专门设计了 RedLock 算法来解决这个问题,向多个独立的 Redis 实例同时加锁,多数派成功才算拿到锁,听起来很靠谱。
但如果你最近看过 Redisson 的源码或文档,会发现 RedLock 相关的 API 已经被标记为废弃了。一个由 Redis 作者亲自设计的分布式锁算法,为什么会被最主流的 Redis Java 客户端给废弃?
RedLock 算法的核心思路
先简单回顾一下 RedLock 的流程:
- 获取当前时间(毫秒级)
- 依次向 N 个独立的 Redis 实例发送加锁请求(SET key value NX PX timeout)
- 如果在大多数实例(N/2 + 1)上加锁成功,并且总耗时没超过锁的过期时间,则认为加锁成功
- 如果加锁失败,向所有实例发送解锁请求
假设有 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 也不甘示弱,写了一篇回应文章来反驳。他的核心论点是:
- GC 停顿问题:可以在执行业务操作前再检查一次锁是否还有效(检查剩余时间)
- 时钟问题:合理配置 NTP,使用 adjtime 而不是 settimeofday,时钟跳变不应该发生
- 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)去做需要共识保证的事情。定位不清晰,自然就尴尬了。
我是小富,下期见。
