想象你经营一个只有五个车位的网红停车场。每天早高峰,几十辆车同时涌进来抢同一个“黄金车位”——车位背阴、离电梯近、还有人专门帮你擦玻璃。问题是:这个黄金车位一次只能停一辆车,谁来停?停多久?万一停车的人临时离开了,别的车是干等着,还是直接把他的车拖走? 分布式锁,就是这些“抢车位规则”的计算机版本。在分布式系统里,无数台服务器(车)会同时争抢一份共享资源(车位)——可能是数据库里的一行库存、一个订单号,也可能是一个 GPU 训练任务。如果没有规则,两辆车同时停进同一个车位,就会发生“超卖”“重复扣款”这类事故。 这篇文章要讲三种主流的“停车场管理方案”:Redis 的“反光锥占位”、Redlock 的“连锁停车场五店同放”、ZooKeeper 的“智能取号排队”。它们各自用什么招数防止死锁、怎么应对故障、在什么场合会掉链子——读懂这些,你就能在架构设计中做出靠谱的选择。

岗位关联

分布式锁是微服务与高并发架构的“安全气囊”。掌握 Redis/Redlock/ZK 三种方案的原理与边界,能让你在架构评审、故障排查中一眼看穿死锁与超卖风险,这是分布式工程师和架构师的核心基本功。

目录
  1. 可视化演示
  2. 锁的问题本质
  3. Redis基础锁:反光锥与荧光定时贴
  4. 看门狗续约:撕掉旧贴纸,贴上新贴纸
  5. Redlock算法:连锁停车场的多数投票
  6. ZK临时顺序锁:智能取号机与接力棒
  7. 方案选型对比:没有最好的停车场
  8. 生产实践与故障复盘:锁失效后的三道保险
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Redis SET NX PX加锁流程,Redlock多节点投票获取多数锁,以及Redis/ZK/Etcd三种分布式锁实现的性能对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

加锁流程 SET NX PX NX+PX=互斥+过期 仅当key不存在时设置成功,保证互斥并自动过期。

锁的问题本质

那个黄金车位每天早高峰都有两辆车同时抵达。如果管理员不在,两辆车就都挤进去——车门刮花、出口堵死。在计算机里,这个“黄金车位”就是临界资源:数据库里的一行记录、一个计数器、一个任务配额。多个线程或服务同时读写它,就叫竞态条件,结果往往是数据错乱。

单机时代,Java 的 synchronized 或操作系统锁就能搞定——因为所有线程共享一块内存,锁状态写在哪里大家都看得见。但到了微服务时代,抢车位的“司机”分布在不同的服务器上,谁也不认识谁,无法靠一块共享内存沟通。于是我们需要一把“看得见、摸得着、放在公共区域”的锁。

分布式锁必须满足三个基本要求:互斥性(同一时刻只有一个客户端持有)、防死锁(持有者崩溃后锁能自动释放)、可重入(同一客户端可重复获取)。这三点分别对应“车位上只有一辆车”“车主消失后管理员能清理”“自己人回头再停不用重新排队”。

所以,分布式锁本质上是在“没有共同管理员”的停车场里,设计一套能让所有车互相信任的占位规则——这套规则怎么设计,决定了 Redis、Redlock、ZK 三种方案的命运。

Lock⇒{M互斥, D防死锁, R可重入}\text{Lock} \Rightarrow \{ M_{\text{互斥}},\ D_{\text{防死锁}},\ R_{\text{可重入}} \}
这把“公共的锁”必须同时做到:独占、自动清理、不挡自己人——三个条件缺一不可。
  1. 互斥性(M)任一时刻至多一个客户端持有该锁,否则两个线程同时写资源必然出错。
    M:任一时刻至多一个持有者M: \text{任一时刻至多一个持有者}
  2. 防死锁(D)持有者崩溃或网络断开后,锁必须在有限时间内自动释放。
    D:∃T,持有超过 T 自动释放D: \exists T, \text{持有超过 } T \text{ 自动释放}
  3. 可重入(R)同一线程在持锁期间再次申请同一把锁时,不会被自己挡住。
    R:同一线程可重复加锁与解锁R: \text{同一线程可重复加锁与解锁}

Redis基础锁:反光锥与荧光定时贴

最简单粗暴的方案:管理员在黄金车位上放了一箱荧光锥。每辆车要停,先跑到车位边问一句:“有锥吗?”没有,就把贴着自己车牌号的锥放上去,停车;有,就在旁边等。

Redis 的 SETNX 就是这个“放锥”动作——SET if Not eXists。执行 SET key value NX,如果 key 不存在就设置成功并返回 OK,相当于锁被抢到;如果 key 已存在就返回空,相当于车位已被占。value 放什么?放一个全局唯一的客户端标识(比如 UUID),就像锥上贴的车牌号。

但进程可能中途崩溃,锥一直放那儿,后面的车永远停不了——这就是死锁。所以必须给锥贴一张“荧光定时贴”:TTL(过期时间)。到期了锥自动消失,锁自动释放。注意:SETNX 和 EXPIRE 如果分两条命令执行,中间进程一崩,定时贴就没了。正确做法是用 SET key value NX EX 30 一条命令搞定,或者用 Lua 脚本保证原子性。同理,释放锁不能直接 DEL——要先比对 value 是不是自己的车牌,再用 Lua 保证“比对 + 删除”原子,否则会把别人的锥扔掉。

荧光锥加定时贴,解决了“谁占位”和“占多久”两个问题。但 Redis 毕竟只是一个停车场——主库挂了怎么办?定时贴谁来撕了重贴?这两个问题,把我们带向看门狗和连锁停车场。

SET order:001 uuid NX EX 30⇒{OK,key 不存在nil,key 已存在SET\ \text{order:001}\ uuid\ NX\ EX\ 30 \Rightarrow \begin{cases} OK, & key\ \text{不存在}\\ nil, & key\ \text{已存在} \end{cases}
Redis 用一条命令同时完成“占位判断”和“过期设置”,从根上消除了中间态。
  1. SET 命令Redis 的基础写命令,这里同时携带三个选项,一次网络往返完成。
    SET key valueSET\ key\ value
  2. NX 条件仅在键不存在时设置——这是互斥性的判断开关。
    NX⇒若 key 不存在才写入NX \Rightarrow \text{若 key 不存在才写入}
  3. EX 过期同时设置 TTL,到期自动删除——这是防死锁的开关。
    EX 30⇒30s 后自动删除EX\ 30 \Rightarrow 30s\ \text{后自动删除}
  4. 原子性NX 与 EX 同属一条命令,Redis 单线程执行,命令不可拆分。
    原子⇒不存在中间态\text{原子} \Rightarrow \text{不存在中间态}

看门狗续约:撕掉旧贴纸,贴上新贴纸

荧光定时贴只能烧 30 秒,可你的车要做大保养,需要 2 分钟。难道贴纸一烧完,后面的车就能把你的车拖走?停车场老板想了个办法:雇一条狗盯着贴纸,快烧完就撕掉重新贴一张——只要你还在车位上,贴纸就永远续上。

这条狗就是看门狗(Watchdog)线程。以 Java 的 Redisson 客户端为例:默认锁的 leaseTime 是 30 秒,拿到锁后看门狗每 10 秒检查一次,如果锁还在持有中,就用 Lua 脚本把过期时间重置回 30 秒。业务线程还没干完,锁就不会提前到期。

这里有个关键权衡:过期时间设多长?设太长,持有者真崩了,其他服务要干等很久;设太短,业务稍微抖动,锁就提前释放,另一个线程进来同时写资源。业界实践是“业务平均耗时的 3-5 倍 + 看门狗续约”,而不是把 TTL 压到极限。另外别忘了主从复制的坑:Redis 主库收到 SETNX 后异步复制给从库,如果主库在复制完成前宕机,从库升主后锁就凭空消失了——单点 Redis 锁在“主挂”场景下会丢锁。

看门狗让“流量高峰不丢锁、持有者崩溃不卡死”成为可能。但狗再敬业,也挡不住主库突然塌了——这时候就得考虑把停车场开成连锁店了。

T剩余≤Tttl2⇒T过期←T当前+TttlT_{\text{剩余}} \le \dfrac{T_{\text{ttl}}}{2} \Rightarrow T_{\text{过期}} \leftarrow T_{\text{当前}} + T_{\text{ttl}}
看门狗的逻辑很简单:剩一半了,就续;续之前先确认锥还是自己的。
  1. 剩余时间判断当锁的剩余有效期不足 TTL 的一半时,看门狗开始行动。
    T剩余≤Tttl2T_{\text{剩余}} \le \dfrac{T_{\text{ttl}}}{2}
  2. 续约动作将过期时间重置为当前时间加上完整 TTL,相当于撕掉旧贴纸、贴上新贴纸。
    T过期←T当前+TttlT_{\text{过期}} \leftarrow T_{\text{当前}} + T_{\text{ttl}}
  3. 前提校验续约前必须校验 value 仍是自己的标识,防止给别人的锁续约。
    若 value=uuid自己 才执行续约\text{若 } value = uuid_{\text{自己}}\ \text{才执行续约}

Redlock算法:连锁停车场的多数投票

单店停车场最大的风险:主收银台的账本丢了,所有“谁占着车位”的记录全没了。连锁停车场老板想了个办法:把同一个黄金车位在 5 家分店里各登记一次,只有至少 3 家分店都确认“这个车位归你”,你才真正获得车位。一家分店的账本烧了,还有两家记得。

这就是 Redlock 算法的思路。它在 5 个互相独立的 Redis 节点上,用同一个 key 和同一个随机 value 执行 SET NX EX。客户端依次尝试 5 个节点,统计成功的数量,同时记录总耗时。成功条件有两个:成功节点数 ≥ 3(大多数),并且总耗时小于锁的有效期——否则你这边刚凑够 3 票,锁已经过期了,等于拿着过期的停车券停车。

Redlock 有个著名争议:它假设各节点时钟是同步的,但现实世界会时钟漂移(NTP 校准、手动调时间),节点 A 觉得过期时间还剩 10 秒,节点 B 觉得已经过了 5 秒。另外,持有锁的进程如果发生长时间的 GC 停顿(JVM Stop-The-World),停顿期间锁到期被别的进程抢走,恢复后原进程还以为自己持着锁——这就是 Martin Kleppmann 与 Redlock 作者 Antirez 之争。实践中的补充方案是隔离令牌(fencing token):每次成功获取锁时递增一个序号,写数据时带上这个序号,下游存储拒绝旧序号的写入,就像“停车券上印着第几批,错过批次的券一律不认”。

连锁停车场用“多数分店记忆”对冲了单店账本丢失,但它仍然依赖所有分店的时钟都准。没有完美方案,只有把风险窗口压到可接受的范围。

Q=⌊52⌋+1=3Q = \lfloor \tfrac{5}{2} \rfloor + 1 = 3
获锁  ⟺  N成功≥3 ∧ Δt<Tttl\text{获锁} \iff N_{\text{成功}} \ge 3 \ \wedge\ \Delta t < T_{\text{ttl}}
5 个节点里至少 3 个同意,且整个投票过程不能拖过锁的寿命——两个条件同时满足才算赢。
  1. 法定多数5 个节点要取超过一半才算数,计算可得至少 3 个。
    Q=⌊N2⌋+1=3Q = \lfloor \tfrac{N}{2} \rfloor + 1 = 3
  2. 票数条件成功写入的节点数必须达到法定多数。
    N成功≥3N_{\text{成功}} \ge 3
  3. 时间条件从开始尝试到选票结束的总耗时,必须小于锁的有效期。
    Δt<Tttl\Delta t < T_{\text{ttl}}

ZK临时顺序锁:智能取号机与接力棒

第三家是智能停车场:不用反光锥,改在入口放一台取号机。车到了先取号,号码小的先进。更妙的是,服务台和你的手机连着——如果超过约定时间联系不上你(会话断了),系统自动把你排的号作废,后面的车依次顶上。绝对不会出现“人走了号还占着”的尴尬。

ZooKeeper 提供两种节点特性,组合起来正好是分布式锁:临时节点(EPHEMERAL)——客户端会话断开时自动删除;顺序节点(SEQUENTIAL)——创建时自动附加全局递增序号。在同一个锁目录下创建临时顺序节点,序号最小的节点就是锁的持有者。其他节点只需要监听自己前一个节点的删除事件,前一个消失,就轮到自己了。

ZK 锁有个经典陷阱叫羊群效应:如果所有排队节点都去监听“最小节点”被删除,一旦锁释放,所有排队的进程同时被唤醒,大家一起往 ZK 上写请求——瞬间打爆集群。正确做法是每个节点只 watch 自己前面的那个节点,像接力棒一样逐个传递。此外,临时节点的生命周期绑定会话(session),如果持有者 GC 停顿导致心跳超时,会话过期,锁也会被自动清理——这一点和 Redis 锁的“过期”殊途同归。但 ZK 的优势在于:底层是 ZAB 协议 + 半数落盘,不存在 Redis 主从异步复制那种“主挂丢锁”问题。

智能停车场用“取号 + 会话心跳”换来了公平排队和强一致,代价是每次取号都要找服务台登记、更新队列——慢。这就引出了三种方案怎么选的问题。

持锁者=min⁡{S1,S2,…,Sn}\text{持锁者} = \min\{ S_1, S_2, \ldots, S_n \}
Si 只监听 Si−1, Si−1 删除后轮到 SiS_i\ \text{只监听}\ S_{i-1}\text{,}\ S_{i-1}\ \text{删除后轮到}\ S_i
排队公平的关键:号码最小的先停,后面的人只盯住前面的那一辆,而不是盯着全场。
  1. 最小序号持锁所有等待者中序号最小的节点成为锁持有者。
    持锁者=min⁡{Si}\text{持锁者} = \min\{ S_i \}
  2. 顺序监听每个节点只 watch 前驱节点,避免所有节点同时被唤醒。
    Si→watch(Si−1)S_i \rightarrow \text{watch}(S_{i-1})
  3. 前驱删除前驱释放或被清理后,当前节点顺势获取锁并执行业务。
    Si−1 删除⇒Si 获得锁S_{i-1}\ \text{删除} \Rightarrow S_i\ \text{获得锁}

方案选型对比:没有最好的停车场

三个停车场摆在你面前:路边摊式 Redis 便宜快速但账本单薄;连锁式 Redlock 稳妥可靠但要多店协调;智能式 ZK 公平有序但进门排队久、服务台设备贵。选哪个,取决于你的车有多贵、堵车能忍几分钟。

从数据面看:Redis 单机锁性能最高(微秒级),但一致性最弱,主备切换可能丢锁;Redlock 需要 5 次网络往返加多数判定,性能约为 Redis 的 1/5,抗单点故障更强,但引入了时钟依赖;ZK 每次创建顺序节点都要跨 ZAB 半数落盘,吞吐量通常是几百到几千 QPS 级别,远低于 Redis,但强一致性最让人安心。

更重要的判断维度是业务需求:可容忍“极端情况下锁失效、靠幂等兜底”→ 选 Redis;需要“跨机房容灾 + 不依赖单点”→ 选 Redlock;要求“强一致、严格公平排队、不可丢锁”且并发量不高 → 选 ZK。没有银弹,选型就是给“性能、一致性、运维成本”三个指标打分。

选型没有“最好的停车场”,只有“最适合你车况的停车场”。想清楚自己最不能承受哪种事故,答案自然浮现。

选型={Redis,允许极端情况短暂失效且追求性能Redlock,需要多数容灾且各节点时钟可控ZK,强制一致且并发量可控\text{选型} = \begin{cases} Redis, & \text{允许极端情况短暂失效且追求性能}\\ Redlock, & \text{需要多数容灾且各节点时钟可控}\\ ZK, & \text{强制一致且并发量可控}\end{cases}
选型不是选“最好的锁”,而是选“最匹配你容错底线”的锁。
  1. Redis 分支性能优先、可容忍小概率丢锁的业务,选单机 Redis。
    Redis⇒高吞吐+弱一致Redis \Rightarrow \text{高吞吐} + \text{弱一致}
  2. Redlock 分支单点已不可接受,愿意为多数投票付出 5 倍网络开销。
    Redlock⇒多数容灾+时钟依赖Redlock \Rightarrow \text{多数容灾} + \text{时钟依赖}
  3. ZK 分支一致性压倒一切,接受较低的写入吞吐。
    ZK⇒强一致+低吞吐ZK \Rightarrow \text{强一致} + \text{低吞吐}

生产实践与故障复盘:锁失效后的三道保险

停车场建好了,规则也定了,但真正考验管理员的是一次真实的早高峰事故。有一辆车停进黄金车位后突然爆胎(进程卡死),看门狗没来得及续约,锁过期了,另一辆车停了进来。两辆车同时占位——处理问题的关键从来不只是“锁”,而是“锁失效后怎么办”。

这正是生产实践的核心:分布式锁不是万能的,必须配上三道保险。第一道是锁粒度:能锁“订单号”就不要锁“整个订单表”,能把库存按 SKU 分段锁就不要一把大锁锁死所有商品,锁粒度越细,并发度越高。第二道是超时降级:执行业务前设定整体超时,超过兜底时间就放弃或走降级路径,避免无限等待。第三道是幂等兜底:即使锁真的失效了,重复执行带来的写入也要能自洽——比如扣库存用“库存 ≥ 数量”条件更新,下单用唯一订单号防重,让数据库成为最后一道防线。

举个真实复盘案例:某电商平台做秒杀,最初用 Redis 单锁 + 只看 TTL,大促时主库抖动导致主从切换,锁全部丢失,库存被两个服务同时扣减,出现超卖。复盘后的方案是:Redis 锁 + 看门狗续约 + 扣减用条件更新(UPDATE stock SET num = num - 1 WHERE num >= 1)兜底 + 订单写唯一索引。四条措施叠加,即使锁失效,数据库的条件更新也会拒绝超卖。这就是“锁负责效率,数据库负责正确性”的经典组合。

停车场最重要的不是锥有多新,而是“出事之后 5 分钟内能不能把车挪走”——分布式锁同样如此。锁只是拦截并发冲锋的第一道闸门,幂等、超时、降级才是最终的安全网。

Tbiz<Tttl×η (η≈0.8)<TfallbackT_{\text{biz}} < T_{\text{ttl}} \times \eta \ (\eta \approx 0.8) < T_{\text{fallback}}
三个时间要排好队:业务干完 < 锁到期 < 兜底超时,顺序不能乱。
  1. 业务时长余量业务执行时间要显著小于 TTL,给锁留出安全余量。
    Tbiz<Tttl×ηT_{\text{biz}} < T_{\text{ttl}} \times \eta
  2. 兜底超时整体兜底超时要大于 TTL,保证“锁先释放,兜底后触发”,不会出现兜底线程还在等锁。
    Tttl<TfallbackT_{\text{ttl}} < T_{\text{fallback}}

AI 应用场景

大模型训练任务调度:给 GPU 上锁

阶段:ML 平台 / 任务调度与资源管理

解决的问题:多个实例同时调度同一个 GPU 节点、重复拉起同一训练任务,会导致显存冲突或重复计费。传统方式是“人肉排期 + 数据库标志位”,并发一高就出现竞态。用 Redis SETNX 锁给每个 GPU 加锁,任务持有锁后才能分配资源,训练结束释放。

具体案例:某 AI 平台 100 个训练任务并发申请 8 张 A100,调度器用 Redlock 保证跨机房不重复分配。锁 value 中写入任务 ID 和隔离令牌,模型保存时校验令牌,避免旧任务把新任务的 checkpoint 覆盖。

特征平台在线更新:让特征版本排队发布

阶段:特征工程 / 在线特征库写入

解决的问题:多个特征回刷任务同时更新同一特征版本,会写坏线上 Serving 缓存。ZK 临时顺序锁让回刷任务按序排队,强一致保证特征版本切换不跳跃。

具体案例:某推荐系统午夜做特征大版本更新,40 个回刷 Job 同时启动。用 ZK 顺序锁排队,Job 失败时会话断开自动清理,后面的 Job 依序执行,特征版本按序发布,线上推理无抖动。

大促秒杀库存扣减:预测流量后的并发保护

阶段:智能运维 / 容量预测后的高并发保护

解决的问题:大促前用 AI 预测模型算出峰值 10 万 QPS,库存扣减必须不超卖。单纯靠 Redis 锁不够,要配条件更新与唯一索引做幂等兜底。

具体案例:某电商 618 秒杀,Redis 锁因主从切换失效一次,但 UPDATE stock SET num=num-1 WHERE num>=1 拒绝了负库存,订单唯一索引拦截了重复提交,全程零超卖。

Python 演示

python86 行
# 模拟三种分布式锁的核心语义:SETNX/TTL、Redlock 多数投票、ZK 顺序排队import uuid class RedisLock:    """模拟 Redis SET value nx ex 语义的锁"""    def __init__(self, ttl=5.0):        self._store = {}        self.ttl = ttl     def try_acquire(self, key, owner, now):        if key not in self._store or self._store[key][1] <= now:            self._store[key] = (owner, now + self.ttl)            return True        return False     def renew(self, key, owner, now):        if key in self._store and self._store[key][0] == owner:            self._store[key] = (owner, now + self.ttl)            return True        return False     def release(self, key, owner):        if key in self._store and self._store[key][0] == owner:            del self._store[key]            return True        return False class Redlock:    """5 个节点中至少 3 个成功才算持锁"""    def __init__(self):        self.nodes = [RedisLock(ttl=2.0) for _ in range(5)]     def try_acquire(self, key, owner, now):        ok = 0        for node in self.nodes:            if node.try_acquire(key, owner, now):                ok += 1        return ok >= 3     def release(self, key, owner):        for node in self.nodes:            node.release(key, owner) class ZkLock:    """序号最小的排队者持锁,前驱释放后继续推进"""    def __init__(self):        self.queue = []     def enqueue(self, owner):        seq = len(self.queue) + 1        self.queue.append((seq, owner))        return seq     def acquire_or_wait(self, key, owner):        seq = self.enqueue(owner)        return seq == 1     def release(self, key, owner):        for i, (seq, o) in enumerate(self.queue):            if o == owner:                del self.queue[i]                return True        return False def demo():    owner = uuid.uuid4().hex    lock = RedisLock(ttl=1.0)    now = 10.0    print('Redis 获取锁:', lock.try_acquire('order:001', owner, now))    print('Redis 重复获取被拒:', lock.try_acquire('order:001', 'B', now))    now += 1.2    print('TTL 过期后 B 获取成功:', lock.try_acquire('order:001', 'B', now))    print('A 释放自己的锁失败:', lock.release('order:001', owner))     rl = Redlock()    print('Redlock 五节点三票通过:', rl.try_acquire('gpu:0', 'C', 0))    rl.release('gpu:0', 'C')     zk = ZkLock()    print('ZK 第一位持锁:', zk.acquire_or_wait('slot', 'X'))    print('ZK 第二位排队:', zk.acquire_or_wait('slot', 'Y'))    zk.release('slot', 'X')    print('ZK 释放后队列长度:', len(zk.queue)) if __name__ == '__main__':    demo()
目的:用纯 Python 在内存中复刻 Redis SETNX 锁、Redlock 多数投票、ZK 顺序队列三种方案的判定逻辑,直观对比它们的互斥与容错行为。
  1. self._store[key] = (owner, now + self.ttl)模拟 Redis 写入键值并附带过期时间,等价于 SET key value NX EX。
  2. if key not in self._store or self._store[key][1] <= now:键不存在或已过期才能获取,这就是 NX + TTL 的判定逻辑。
  3. if key in self._store and self._store[key][0] == owner:释放前校验持有者身份,防止误删别人的锁,对应 Lua 脚本的“比对 + 删除”。
  4. ok >= 3Redlock 的法定多数:5 个节点中至少 3 个成功才算持锁。
  5. seq = len(self.queue) + 1模拟 ZK 顺序节点的全局递增序号。
  6. return seq == 1最小序号者获得锁,其余返回 False 表示排队等待。
  7. del self.queue[i]模拟临时节点被删除后队列推进,下一辆车自动顶上。

常见误区

误区我用 Redis 的 SETNX 拿到 key,锁就到手了,从没想过进程崩溃、锁过期、别人误删这些问题。

正解SETNX 只是“占位”的第一步。安全地使用它需要四件事同时成立:命令原子(SET NX EX)、value 携带唯一标识、设置合理 TTL、配合看门狗续约。就像反光锥放好了,但锥上的定时贴可能烧完、别人可能把你的锥扔了。

为何常见:单机 Redis 操作“看起来瞬间完成”,容易让人忽略分布式环境里的故障模式。

误区主库挂了自动切到从库,锁一定还在,所以不需要 Redlock 那套复杂的多节点方案。

正解Redis 主从是异步复制。主库刚执行完 SETNX 还没复制到从库就宕机,从库升主后锁就丢了。Redlock 用多节点多数投票对冲的正是这种丢锁风险。

为何常见:对主从复制的异步性理解不足,误以为主从是“同步”的。

误区Redlock 有 5 个节点 + 多数同意,理论完备,生产环境用了就高枕无忧。

正解Redlock 在时钟回拨、进程停顿、网络分区组合拳下仍有安全窗口。它降低了单点故障概率,但没有消除所有风险。对极端一致性场景,还要叠加隔离令牌(fencing token)。

为何常见:“多数投票”听起来像 Paxos/Raft,让人误以为 Redlock 达到了同样的强一致级别。

误区害怕锁卡死,把过期时间设成 100ms,结果业务稍微慢一点锁就没了,另一个线程冲进来把数据写坏。

正解TTL 过短,业务未完成锁就被抢走;过长,故障恢复慢。正确做法是“业务耗时留 3-5 倍余量 + 看门狗续约”,而不是靠极端值走钢丝。

为何常见:只看到“锁过期能防死锁”,没看到“锁提前过期会破坏互斥”。

误区ZK 强一致、临时节点自动清理,所以任何场景都应该用 ZK 锁。

正解ZK 每次写都要走 ZAB 半数落盘,吞吐远低于 Redis,还要额外维护 ZK 集群。性能敏感场景 Redis 更合适;强一致低吞吐场景 ZK 更稳。各有适用边界。

为何常见:拿“一致性等级”这一个维度做选型,忽略了性能与运维成本。

面试题

Q1. 在 Redis 中,SETNX 命令的核心语义是?

单选题 · easy

  • A.键不存在时设置并返回成功← 正确答案
  • B.键存在时强制覆盖旧值
  • C.无论键是否存在都设置
  • D.设置后立即自动过期

答案:A。SETNX 即 SET if Not eXists,只有键不存在时才写入,这是分布式锁互斥性的基础。B 是覆盖语义,C 是普通 SET,D 是 EX 选项的职责,均不符。

Q2. 为什么 SETNX 与 EXPIRE 分两步执行有风险?

单选题 · easy

  • A.两步之间进程崩溃留下死锁← 正确答案
  • B.两步都会占用大量网络带宽
  • C.Redis 会拒绝执行两步命令
  • D.两步执行结果必然不一致

答案:A。先 SETNX 成功、还没执行 EXPIRE 时进程崩溃,锁没有过期时间,永远占着车位。这就像只放了反光锥却没贴定时贴。正确做法是用 SET key value NX EX 30 或 Lua 脚本保证原子性。

Q3. 分布式锁的三大基本要求中,不包括?

单选题 · easy

  • A.互斥性保证同一时刻独占
  • B.死锁防护防止锁永不释放
  • C.可重入允许同一线程重入
  • D.锁必须具备跨机房同步能力← 正确答案

答案:D。三大要求是互斥、防死锁、可重入。跨机房同步是部署层面的需求,不是分布式锁的基本属性。就像停车场管理员的核心职责是“只停一辆车、人走能清理、自己人方便”,而不是“分店必须开遍全国”。

Q4. Redlock 算法中,5 个节点至少需要几个同意?

单选题 · mid

  • A.至少需要全部五个节点同意
  • B.至少需要三个节点表示同意← 正确答案
  • C.只需要两个节点表示同意
  • D.只需要主节点同意即可通过

答案:B。法定多数 Q = ⌊5/2⌋ + 1 = 3。全部同意要求太高,降低可用性;2 个同意会在网络分区时出现两把锁同时成立。连锁停车场至少要多数分店记得“车位归你”才算数。

Q5. Redlock 最著名的理论争议来自?

单选题 · mid

  • A.数据库复制延迟导致数据丢失
  • B.时钟漂移和停顿破坏互斥性← 正确答案
  • C.网络分区导致锁永远超时
  • D.内存溢出导致节点全面崩溃

答案:B。Martin Kleppmann 指出 Redlock 依赖各节点时钟同步,时钟漂移、GC 停顿会使锁在“自以为持有”时其实已过期,破坏互斥性。这正是 Redlock 安全边界的核心争议。

Q6. ZooKeeper 实现分布式锁常用的节点类型是?

单选题 · mid

  • A.持久节点且不附带任何序号
  • B.临时顺序节点并比较序号大小← 正确答案
  • C.持久顺序节点并比较序号大小
  • D.容器节点并监听全部子节点

答案:B。临时节点保证会话断开自动删除(防死锁),顺序节点保证全局递增序号(公平排队),两者结合即 ZK 锁的经典组合。持久节点不会自动清理,不适合做锁。

Q7. 看门狗(Watchdog)在 Redis 锁中的作用是?

单选题 · mid

  • A.在锁即将过期前自动续约保活← 正确答案
  • B.监控其他线程的锁并强制清理
  • C.将 Redis 的操作日志实时备份
  • D.检测网络延迟并重连主库节点

答案:A。看门狗就是那条“盯着荧光贴、快烧完就重贴”的狗。Redisson 默认每 10 秒检查一次,在剩余 TTL 不足一半时将过期时间重置,保证业务耗时超过初始 TTL 时锁不提前释放。

Q8. 某业务平均执行 800ms,异常峰值 3000ms,应如何设置 TTL?

单选题 · hard

  • A.TTL 设为 800ms 保证快速释放
  • B.按峰值 3000ms 设置并配看门狗← 正确答案
  • C.TTL 固定 5000ms 且不做续约
  • D.不设置 TTL 由业务自行释放

答案:B。TTL 过短会在峰值时提前释放锁,破坏互斥;过长则崩溃后恢复慢。正确做法是按峰值留余量(如 3000ms),并配看门狗在长尾场景续约。不设 TTL 则崩溃后必然死锁。

Q9. 以下哪些情况会导致 Redis 分布式锁失效?

多选题 · mid

  • A.主从切换后新主上锁记录丢失← 正确答案
  • B.持有锁线程发生长时间 GC 停顿← 正确答案
  • C.锁提前过期被其他线程抢占← 正确答案
  • D.键空间命中率下降或缓存失效

答案:ABC。主从异步复制会丢锁,GC 停顿会让锁在持有时过期,TTL 过短会让锁被提前抢占——三者都会打破互斥。键空间命中率是缓存性能指标,与锁的失效无关。

Q10. 关于 Redlock 的安全边界,以下说法正确的有?

多选题 · extreme

  • A.进程 GC 停顿期间锁可能过期← 正确答案
  • B.隔离令牌可阻止旧任务写数据← 正确答案
  • C.Redlock 依赖时钟不被回拨← 正确答案
  • D.Redlock 在异步网络下绝对安全

答案:ABC。GC 停顿会让持锁者失去对锁的感知,锁过期被抢后原进程继续写数据——此时隔离令牌(fencing token)用递增序号让下游拒绝旧写入;时钟漂移/回拨会破坏多数判定,说明 Redlock 并非绝对安全。异步网络下它仍有安全窗口,D 错误。