RedLock 是我见过最典型的"看似有标准答案、其实没有标准答案"的分布式系统面试题。很多候选人能三五句话背出"五个 Redis 实例、半数以上加锁成功、TTL 过期释放"这套流程,但一被追问"那它到底可靠吗?为什么有人强烈反对?"就卡壳了。
原因不怪大家。RedLock 的可靠性不是一个能靠背题解决的问题,它牵扯到分布式系统里最底层的三个主题:时间、故障、一致性。同样一套算法,在理论派眼中满是漏洞,在工程派手中却确实解决了不少线上问题。这篇文章我会把它拆成一条完整的分析链路——从算法本身讲起,到可靠性成立的边界,再到现实中哪些因素会击穿它,最后落到面试现场和工程选型。内容会持续补充,因为分布式锁这个话题每深挖一层都有新东西。
1. 从单集群到五节点:RedLock 要补的课
要理解 RedLock,先得回头看它出现之前的"常规操作"有多脆弱。最早做 Redis 分布式锁的团队,基本都踩过同一串坑。
1.1 单机锁的三宗罪
最朴素的加锁就是一条命令:
SET lock_key unique_value NX PX 30000NX保证只有 key 不存在时才能写入,PX设置过期时间,这已经是"能用"的雏形。但把它放到真实分布式环境里,问题立刻浮出水面:
- 加锁与过期设置不是原子的。老式写法是
SETNX加锁,再单独EXPIRE设过期,两步之间进程一崩,锁就永久不释放。 - 锁会被别人误删。客户端 A 加锁后业务超时,锁自动过期;客户端 B 加锁成功;这时 A 业务结束执行
DEL,把 B 的锁删了,于是 C 也能进来了。 - 主从切换丢锁。锁写入主节点,主节点还没来得及把数据同步给从节点就宕机,哨兵把从节点提升为主,此时新的主节点根本没有这条锁记录,另一个客户端轻松加锁成功。
这些问题里,前两个用"随机值 + Lua 脚本保证原子性"就能解决,第三个却是单节点架构的死穴。只要复制是异步的,丢锁窗口就不可能完全消除。所以 RedLock 的思路很直接:既然一个节点不靠谱,那就让多个互不相关的节点同时背书。单点上的失败不再代表整体失败,这是它和普通 Redis 锁最本质的区别。
1.2 RedLock 算法五分钟速览
RedLock 的流程可以压缩成五步:
- 客户端获取当前机器时间,向N 个(通常为 5 个)相互独立的 Redis 节点同时尝试加锁,使用同一个 lock_key 和同一个唯一随机值。
- 每个加锁请求都要设置一个远小于锁 TTL 的请求超时,比如 TTL 是 30 秒,单节点请求最多等 100 毫秒。这样某个节点宕机时,客户端不会无限期阻塞在那里干等。
- 客户端计算加锁总耗时:如果成功加锁的节点数 ≥ N/2 + 1(多数派),且总耗时小于锁 TTL,才认为锁真正获取成功。
- 锁一旦成功,持有时间就是 TTL;如果业务没跑完,需要客户端自己续期。
- 释放时,对所有节点发送同一个 Lua 脚本,用随机值做校验再删除:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end你可能会问,为什么加锁命令和释放脚本对节点数量的要求不一样——加锁要靠多数派,释放却要求"所有节点都执行一遍"?原因是释放操作不涉及竞争:多删一次无所谓,少删一次才致命。只要那个节点上还留着锁记录,它就可能阻止其他客户端加锁,所以必须全量尝试。这是 RedLock 设计里一个容易被忽视的细节。
1.3 "为什么是五个节点"背后的容错数学
RedLock 原论文给的是 5 个,这其实不是玄学。看这张表就明白:
| 节点总数 | 法定人数 | 最多可容忍故障节点数 |
|---|---|---|
| 3 | 2 | 1 |
| 5 | 3 | 2 |
| 7 | 4 | 3 |
5 个节点能容忍 2 个同时宕机,同时还能凑出 3 个多数派;7 个虽然容忍度更高,但加锁请求数、网络开销、运维成本都上去了,边际收益却不高。而 4 个、6 个这种偶数节点没有意义,因为多数派计算需要取整,偶数和奇数(在容错上限上)表现相同,成本却更高。所以在成本和可靠性之间,5 是一个很务实的折中。
另一个关键点:这 5 个实例必须尽量物理隔离——不同机架、不同交换机、最好不同机房。如果它们全部署在同一台物理机上,一个断电全带走,锁的"多数派"瞬间变成零个,整个算法就名存实亡了。
2. 多数派互斥是怎么证明的(以及证明成立的前提)
很多人对 RedLock 的理解停留在"复制流程"层面,但真正能让面试官眼前一亮的是你能解释清楚:这算法在理想条件下凭什么能维持互斥。这里有一个非常漂亮的反证法。
2.1 用反证法理解法定人数的意义
假设两个客户端 C1 和 C2 同时拿到了锁。C1 拿锁成功,意味着它在至少 3 个节点上抢到了锁;C2 拿锁成功,也意味着它至少在另外 3 个节点上抢到了锁。两个集合的节点数加起来至少是 6,而一共只有 5 个节点。根据鸽笼原理,C1 和 C2 必然在同一台节点上同时取得了锁。
但每个节点上的加锁操作是原子的,用的是SET lock_key value NX PX,同一个 key 不可能同时被两个不同 value 写入。于是产生了矛盾。结论:两个客户端不可能同时在多数派节点上持有锁——这就是法定人数保护互斥性的数学内核。
听起来天衣无缝,对吧?但注意,这个证明默默预设了几个前提:
- 所有节点的时钟是同步的、单调的,不会突然回拨;
- 所有客户端进程不会在持有锁期间长时间停顿;
- 锁的 TTL 一定大于"客户端实际完成临界区操作所需的时间";
- 网络虽然可能分区,但不会出现"已拿到锁的节点不按真实时间过期"的诡异状态;
- Redis 节点的数据不丢失,不会默默吞掉已写入的锁记录。
这些前提在纸面上都说得通,但在真实系统里一个比一个难保证。这正是下一章要展开的核心:RedLock 的可靠性,不是算法本身藏着问题,而是它依赖的现实假设太强了。
2.2 为什么"总耗时 < TTL"是必须的
加锁成功的判定条件里,那条"总耗时小于锁 TTL"经常被人忽略。它的意义在于:如果客户端在第 1 个和第 2 个节点上花了 15 秒才加锁成功,而这把锁的 TTL 只有 10 秒,那么最早那两个节点上的锁其实已经过期了。此时客户端宣布"我拿到了锁",等于拿到了一把已经开始倒计时的钥匙——它不能保证你对整个临界区的独占,因为锁在临界区开始之前就可能失效了。
所以这条判断不是性能优化,是正确性边界。它把"多数派节点同意"和"同意结果在有效期内达成"这两件事牢牢绑定在一起。面试时能够主动提到这一点,说明你真正吃过分布式系统的亏。
2.3 锁续期:解决"业务跑不完"的标准手段
锁的 TTL 设长了,节点故障后锁要很久才能释放,可用性差;设短了,业务没跑完锁就过期,互斥性被破坏。常见的做法是把 TTL 当成一个"最小租约",业务通过看门狗机制自动续期:每经过 TTL 的 1/3 时间,就把锁的过期时间往后推一个 TTL,直到业务显式解锁。
续期看起来解决了问题,但要记住一个残酷的细节:如果业务线程发生 GC 停顿,看门狗线程同样会被暂停(在 Java 这种 GC 会 STW 的场景下,所有用户线程都停住了),锁照样会过期。续期不是万能药,它只是把失效概率降低了,并没有从原理上消除。
3. 可靠性裂缝清单:停顿、时钟、分区、丢锁的完整推演
这一章是全篇最核心的部分。面试时能清晰说出 RedLock 的失效场景,才算真正理解了它的边界。
3.1 进程停顿:GC 让"旧锁主"复活
考虑一个最经典的时序:
- 进程 A 成功在 5 个节点上加锁,进入临界区之前,JVM 触发了一次 Full GC,进程暂停了 20 秒。
- 锁 TTL 为 10 秒,于是锁自动过期。
- 进程 B 加锁成功,也在所有节点上拿到了锁,进入临界区写数据。
- 进程 A 的 GC 结束,它不知道自己刚才的锁早已过期,继续执行临界区代码,往共享资源里写数据。
结果就是 A 和 B 同时写。这不是 RedLock 独有的问题,而是所有"基于过期时间的分布式锁"的通病。锁并不能让你在"全部持有时间内"独占资源,它只能保证你在"成功拿锁的那一刻"互斥。只要出现过期,旧持有者就可能变成和"僵尸进程"一样的存在——它的锁没了,但它的行为还在继续。
这个问题的本质在于:锁的过期基于时间,而进程的行为基于 CPU 执行进度,这两者没有因果关系。你无法用时间上的过期去管理一个滞后于时间的进程。
3.2 时钟跳跃:Redis 的过期是"物理钟",不是"逻辑钟"
Redis 的 key 过期依赖服务器的系统时钟。NTP 校时、虚拟机时钟漂移、运维手动调整,都可能导致时钟突然回拨或前跳:
- 时钟回拨:某个节点的"现在"退回到了过去,本该过期的锁被顺延了生命周期。客户端在多数派上获得的锁,可能比它以为的更长命。
- 时钟前跳:锁提前过期,客户端还没跑完业务就丢了锁。
更隐蔽的是加锁总耗时判断,它用客户端本地时钟;各节点的 TTL 演化,用服务器本地时钟。三个时钟没有统一的时间基准,互斥性在数学论证时默认的"同一个时间坐标系"根本不存在。这也是理论派批判 RedLock 最重的一拳:它把分布式系统的安全建立在了物理时钟这个最不可靠的假设之上。
3.3 网络分区:多数派也可能分崩离析
网络分区场景下,RedLock 的问题很典型。假设 5 个节点分成两个区域:区域 X 有 3 个,区域 Y 有 2 个。客户端 A 在区域 X 内,它能连通全部 3 个节点,轻松凑够多数派拿锁;客户端 B 在区域 Y 内,只能连通 2 个,拿不到锁,表现正常。
但换个分法:区域 X 有 2 个节点,区域 Y 有 3 个节点,且两个区域之间有客户端分别驻守。A 在 Y 区拿 3 个节点的锁成功,B 其实也在 Y 区,理论上不可能同时成功——因为锁在同一批节点上。真正的隐患是分区愈合后的时间错位:分区期间某个"少数派客户端"以为自己没拿到锁,等分区恢复后可能又按照旧的逻辑继续重试,而多数派节点上的锁数据可能已经被覆盖。这类问题强一致系统里靠租约和纪元号才能压制,RedLock 没有这种机制。
一个需要认清的事实是:RedLock 的容错模型默认**"多数派存活且彼此连通"**。如果网络把节点劈成"3+2",它还能工作;劈成"2+3"且客户端在两边分布,就会进入算法无法保证的状态。
3.4 主从切换与持久化丢锁:数据消失是终极破坏
回到主从复制的老问题上——RedLock 虽然用多个节点规避单点故障,但并没有要求每个节点关闭主从复制、加深持久化。很多团队部署 Redis 时默认开 RDB + AOF(每秒刷盘),这就有个窗口:锁已经写入主节点,但 AOF 还没落盘,或者从节点还没同步,主节点挂了。
从节点晋升后,新主节点里没有锁记录,只在这个"新主"上,客户端 B 就能加锁成功。如果其他几个旧节点上锁还在,B 的加锁请求会在那些节点上失败,最终 B 可能凑不到多数派;但别忘了,锁的可靠性是以"最弱的节点"为下限的——只要有一个节点丢了锁记录并参与了多数派投票,整个互斥保证就失效了。RedLock 没有给每个节点做 Paxos/Raft 级别的持久化,所以"节点故障后数据不丢失"这个前提,它天然不满足。
3.5 一张表总结可靠性击穿路径
| 破坏因素 | 具体表现 | 能否靠修改 RedLock 参数解决 |
|---|---|---|
| GC/进程停顿 | 旧持有者"复活"与其他客户端并发写 | 不能,需要资源端做 token 校验 |
| 时钟回拨/前跳 | 锁的过期时间不可预测 | 不能,需要不依赖物理时钟的机制 |
| 网络分区 | 多数派被切割,互斥失效 | 部分缓解,无法根治 |
| 主从切换/持久化丢数据 | 锁记录消失,新客户端无条件拿锁 | 可以缓解(持久化优化),无法根除 |
| 业务执行超时 | 锁过期但业务还在写 | 看门狗续期缓解,GC 时仍失效 |
4. 一场没有标准答案的争论:锁的"绝对安全"和"工程够用"
RedLock 之所以被反复讨论,是因为它背后站着一场著名的技术争论。我尽量用不偏不倚的方式,把双方的核心逻辑都讲透。
4.1 批判方:没有 fencing token 的锁等于没有门禁
批判方的立场非常清晰:分布式锁真正的目的,不是"让客户端之间互相等着",而是"保护共享资源不被并发的过期请求破坏"。以此为标准,RedLock 存在两个硬伤。
第一,它没有给每个锁持有者一个单调递增的 token。所谓 fencing token,就是一把锁的"编号"。假设资源端能检查 token,每次请求都带上锁的编号,资源端只接受编号更大的请求,那么即使进程 A 的锁已经过期、A 晚于 B 发起了写入,资源端也会因为 A 的 token 小于 B 的 token 而拒绝 A。没有 token,资源端就和"不设防"一样,任何客户端只要手上有锁(哪怕过期了)都能直接写。
第二,GC 停顿场景直接击穿时间假设。停顿时,锁过期、B 拿锁写完、A 苏醒继续写,A 的数据甚至可能覆盖 B 的新数据,形成不可恢复的旧值覆盖新值。这时候你无论把 TTL 调多长,都治标不治本。
批判方给出的方案是在存储层做 fencing:任何写请求必须携带 token,存储端用版本号或特殊字段比对,拒绝旧的 token;锁层则建议使用支持序号/版本语义的强一致存储(比如带版本号的对象存储或支持比较并交换的数据库),而不是纯 Redis。
4.2 算法作者:锁不是安全证明,是协调工具
算法作者的回击同样有力,核心观点可以总结成三点:
第一,fencing token 必须由资源端配合实现。现实中有多少业务资源本身支持 token 校验?大部分分布式系统的资源端就是一个数据库、一套缓存、一个消息队列,加 token 校验意味着要侵入业务代码,成本极高。算法作者的反问是:"如果我的锁之后,资源端什么检查都不做,那再完美的锁也保护不了你。"
第二,锁本来就是道德约束,不是物理约束。就像现实世界的门锁,它不是坚不可摧的金库,而是"阻止无意进入"的仪式。RedLock 的目的是将并发冲突的概率降到极低,在大多数业务里已经够用,而不是在数学上证明万无一失。
第三,一些看似"绝对正确"的方案,在工程中反而会造成更大的可用性风险。强一致存储虽然理论上更安全,但故障时拒绝服务的时间更长,这是另一种代价。分布式系统从来都是在一致性和可用性之间做交易,不能只盯着安全批评。
4.3 为什么双方说得都有道理:问题的前提不同
我自己的判断是:两边其实在讨论两个不同的问题。批判方默认的场景是"资源端完全被动、写错数据不可容忍、故障需要彻底隔离"——在这种前提下,你必须引入 fencing。算法作者的默认场景是"锁只是协调手段、业务可以接受极低概率的冲突、资源端无法改造"——在这种前提下,RedLock 的性价比无可替代。
作为面试者,最重要的不是站队,而是把两边的论证链完整复述出来,然后落到"适用边界"四个字上。面试官真正想听的,往往不是"行"或"不行"的结论,而是你能不能把复杂问题拆成"在什么条件下怎样"的陈述。
5. 面试官的关注点:从算法流程到系统观
5.1 这道题到底在考你什么
表面上看,面试官在考 RedLock 的算法细节;实际上,他在考察你是否具备分布式系统的整体思维。我把考察维度拆成三层:
| 考察层面 | 具体能力 | 表现好的回答特征 |
|---|---|---|
| 第一层:记忆与理解 | 知道 RedLock 流程、参数含义 | 能娴熟写出加锁命令和释放脚本 |
| 第二层:分析与推演 | 能构造失效场景 | 能画出 GC/时钟/分区造成的时序 |
| 第三层:权衡与设计 | 知道"没有银弹",能结合业务选型 | 能把讨论从"选择哪个"变成"在什么约束下选哪个" |
很多候选人在第一层就止步了,背得越好,被问到第二层越窘迫。而这道题之所以被高频采用,正是因为它能准确区分"背答案的人"和"建立过系统认知的人"。
5.2 一个四层递进的现场回答框架
如果面试官只给你三到五分钟,我建议按这个结构组织回答,而不是一口气把所有细节倒出来:
- 第 1 层(30 秒):一句话定位。RedLock 是 Redis 生态下的一种分布式锁算法,用多个独立 Redis 节点构成法定人数,通过多数派投票保证互斥。
- 第 2 层(1 分钟):讲算法四要素。唯一随机值防误删、
SET NX PX原子加锁、多数派判定、Lua 脚本原子释放。顺带说一句"为什么释放要全量尝试"。 - 第 3 层(2 分钟):主动讲边界。直接说"这个算法的可靠性依赖强假设",挑 GC 停顿和主从丢锁两个例子展开,说明什么时候锁会失效。能主动讲出失效场景,比面试官追问才说好得多。
- 第 4 层(加分项):讲争论和权衡。复述批判方和算法作者的立场,给出你的工程经验:什么时候敢用、什么时候坚决不用。
这个结构的好处是由浅入深且有自主权——你把面试官想问的下一句话提前说出来了,他会顺着你的节奏继续深挖,而不是用连环追问来测试你。
5.3 追问清单:多数人会栽在哪些暗角
| 面试官追问 | 考察点 | 参考作答方向 |
|---|---|---|
| 为什么用 5 个实例而不是 3 个? | 容错数学 | 5 容忍 2 个故障,性价比最优 |
| 释放锁为什么必须用 Lua 脚本? | 原子性认知 | 避免“查询后删除”两步间的错删 |
| TTL 怎么定? | 租约意识 | 业务最长执行时间 + 缓冲,再配看门狗 |
| 看门狗续期能解决 GC 停顿吗? | 模型漏洞 | 不能,GC 会暂停所有线程 |
| 主从切换会丢锁吗? | 复制与持久化 | 会,开启 AOF everysec 只是缩小窗口 |
| 资源端怎么配合才能兜底? | fencing token | 版本号/序号校验,拒绝过期写请求 |
| 如果锁节点集体不可达怎么办? | 可用性权衡 | 等多数派恢复;无法做到可用与安全兼得 |
6. 工程里到底该不该上 RedLock:替换方案与增强手段
面试答题可以停留在理论层,但工程落地需要的是"在已知缺陷面前做决策"的能力。我把自己的实践准则给你:
6.1 一张决策表帮助判断
| 业务特征 | 适合 RedLock | 建议换方案 |
|---|---|---|
| 允许极低概率的并发冲突 | 适合 | — |
| 对写入正确性零容忍(资金、订单状态) | — | 必须强一致锁 + 幂等 |
| 资源端能做版本/token 校验 | 适合,配合 fencing 更稳 | — |
| 资源端完全被动、无法改造 | 勉强可用,风险自担 | 最好不用锁,改用唯一索引/幂等键 |
| 锁长时间持有(分钟级) | 不太适合 | ZK/etcd 租约更稳 |
| 追求低延迟、高吞吐 | 适合 | — |
| 锁节点是单机部署的两个副本 | 不建议 | 无多数派可言 |
6.2 如果你决定用 RedLock,至少做强这四件事
第一,持久化要拉满。至少开启 AOF 且策略为everysec,资金敏感场景甚至考虑always,但要做好性能回退的心理准备。
第二,节点尽量独立。不要让锁节点混用同一个虚拟化宿主机或同一个故障域,否则多数派在物理层面依然等于一个点。
第三,TTL 设置要保守。我给业务设 TTL 时,习惯是"预估最长执行时间 ×1.5 以上",宁可让故障后锁多撑一会儿,也不要让业务中途丢锁。
第四,资源端必须做幂等兜底。锁只是第一道闸,真正靠谱的安全网是接口层的幂等键或版本号校验——锁可以失效,但写操作必须拒绝重复和过期请求。
6.3 更稳的路:ZooKeeper/etcd 的序号与租约
如果需要绝对互斥,或持有时间很长,我会直接换用强一致协调系统。ZooKeeper 的临时顺序节点天然适合做锁:节点一断,临时节点自动删除,客户端通过监听机制抢序,每次拿到的序号天然具备 fencing 语义——资源端只要拒绝序号小于已提交请求的写操作,就真正实现"锁 + token"二合一。etcd 的租约原理类似,且 API 更现代、部署更轻。
代价是可用性:强一致系统在分区时会牺牲可用性来保证安全,这恰恰和 Redis "AP 优先"的哲学相反。选择之前先问自己:你的业务是更怕"锁坏了",还是更怕"锁不可用"?
6.4 我的默认选择
做了这么多年分布式系统,我的默认路径其实很简单:能用业务幂等解决就不上锁,比如给订单号建唯一索引、用状态字段做乐观锁;一定要用分布式锁且并发量不高,优先考虑 ZK/etcd 这类强一致方案;只有确实需要极低延迟、又要避免单点 Redis 崩溃的锁丢失,我才会启用多节点 RedLock,并且让资源端承担最终的幂等校验。
说到底,锁本身不是安全机制,它只是请求入口处的协调器。真正决定数据安全的是你下游那扇门——版本号、幂等键、唯一约束,才是最后一道真正锁得住的锁。理解到这一层,RedLock 这道题的可靠性与边界,自然也就通了。