<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ZooKeeper on 止语Lab</title>
        <link>https://www.wujiachen.com.cn/tags/zookeeper/</link>
        <description>Recent content in ZooKeeper on 止语Lab</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Thu, 04 Jun 2026 11:51:24 +0800</lastBuildDate><atom:link href="https://www.wujiachen.com.cn/tags/zookeeper/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>分布式锁不是选 Redis 就完事了</title>
            <link>https://www.wujiachen.com.cn/posts/distributed-lock-selection/</link>
            <pubDate>Thu, 04 Jun 2026 11:51:22 +0800</pubDate>
            <guid>https://www.wujiachen.com.cn/posts/distributed-lock-selection/</guid>
            <description>&lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/cover.png&#34; alt=&#34;Featured image of post 分布式锁不是选 Redis 就完事了&#34; /&gt;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/cover.png&#34; alt=&#34;封面&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;本文实验基于 Redis 7.x / etcd 3.5.x / ZooKeeper 3.8.x / Go 1.22+&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;&lt;strong&gt;导读&lt;/strong&gt;：Redis SETNX 的三个失败场景，一套诊断框架，帮你做选型。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;0-你的锁该做次体检了&#34;&gt;&lt;a href=&#34;#0-%e4%bd%a0%e7%9a%84%e9%94%81%e8%af%a5%e5%81%9a%e6%ac%a1%e4%bd%93%e6%a3%80%e4%ba%86&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;0. 你的锁该做次体检了&#xA;&lt;/h2&gt;&lt;p&gt;凌晨三点，监控告警炸了——同一个订单扣了两次款。&lt;/p&gt;&#xA;&lt;p&gt;你打开日志，发现两个 Pod 同时拿到了那把&amp;quot;分布式锁&amp;quot;。这画面让人脑壳疼：明明加了 Redis 锁，为什么还出问题？&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/ch0-midnight-alert.png&#34; alt=&#34;凌晨告警&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;我先抛一个反直觉的判断：&lt;strong&gt;你以为加了锁就安全？这是把分布式当单机来想了&lt;/strong&gt;。Redis SETNX 不是&amp;quot;加了就保你&amp;quot;的护身符。它更像一个温度计，能给你显示一个数，但显示出来不等于事就结了。&lt;/p&gt;&#xA;&lt;p&gt;这篇文章不打算给你&amp;quot;Redis vs etcd vs ZK 选哪个最好&amp;quot;的标准答案。我想做的是带你做一次体检——把 Redis 锁在三种场景下会怎么&amp;quot;病&amp;quot;先摆出来。&lt;/p&gt;&#xA;&lt;p&gt;再分析这三种&amp;quot;病&amp;quot;的共因，然后看 etcd 和 ZK 是怎么针对这些根因设计的，最后给你一棵决策树。结尾我不会替你下处方，只把判断条件交到你手上。&lt;/p&gt;&#xA;&lt;p&gt;你需要的前置只有：知道 SETNX 是怎么用的，用过 Redis 主从（或 Sentinel），听说过 etcd 和 ZK。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;1-症状清单三种你可能没意识到的失败场景&#34;&gt;&lt;a href=&#34;#1-%e7%97%87%e7%8a%b6%e6%b8%85%e5%8d%95%e4%b8%89%e7%a7%8d%e4%bd%a0%e5%8f%af%e8%83%bd%e6%b2%a1%e6%84%8f%e8%af%86%e5%88%b0%e7%9a%84%e5%a4%b1%e8%b4%a5%e5%9c%ba%e6%99%af&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1. 症状清单：三种你可能没意识到的失败场景&#xA;&lt;/h2&gt;&lt;p&gt;我把三种场景压在一章里，因为它们不是&amp;quot;独立故障&amp;quot;——后面你会看到，它们其实有共同的根因。&lt;/p&gt;&#xA;&lt;p&gt;每个场景我都用同样的四步呈现：现象 → 复现代码 → 时序 → 为什么。代码全部能在本机跑，evidence 目录里有完整可执行版本（go run ./01-process-crash 那种）。&lt;/p&gt;&#xA;&lt;h3 id=&#34;11-症状一进程崩溃锁卡死直到-ttl&#34;&gt;&lt;a href=&#34;#11-%e7%97%87%e7%8a%b6%e4%b8%80%e8%bf%9b%e7%a8%8b%e5%b4%a9%e6%ba%83%e9%94%81%e5%8d%a1%e6%ad%bb%e7%9b%b4%e5%88%b0-ttl&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1.1 症状一：进程崩溃，锁卡死直到 TTL&#xA;&lt;/h3&gt;&lt;p&gt;最常见的场景。客户端 A 拿到了锁，但执行业务时进程被 kill -9——可能 OOM，可能滚动更新强杀。它来不及调 DEL 释放锁。这把锁会卡在 Redis 里，直到 TTL 自己过期。&lt;/p&gt;&#xA;&lt;p&gt;我跑了个小实验。A SETNX 拿锁 TTL=10s 后什么都不做（模拟崩溃）；B 不停尝试 SETNX，每 200ms 一次。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.00s] client_A SETNX → true   (TTL=10s)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.00s] client_A 模拟 kill -9，不调用 DEL/UNLINK&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.00s] client_B 开始尝试 SETNX，每 200ms 一次&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 10.09s] client_B SETNX → true  ✅ 拿到锁（第 51 次尝试）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;B 等了 10.09 秒，尝试了 51 次。&lt;/p&gt;&#xA;&lt;p&gt;这不是 Redis 的 bug，是设计选择。Redis 默认配置下没有&amp;quot;客户端心跳&amp;quot;机制，它没法知道你是真在干活还是已经死了，只能让 TTL 来兜底。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/ch1-e1-ttl-wait.png&#34; alt=&#34;等满 TTL&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;你能做的是把 TTL 调短。但这又会带来另一个问题：如果业务跑得比 TTL 长，锁还没用完就过期了，让别人误抢。这就引出第二个症状。&lt;/p&gt;&#xA;&lt;p&gt;而且 TTL 这个值你没法做&amp;quot;绝对正确&amp;quot;。设 1 秒，业务偶尔 1.2 秒就翻车；设 30 秒，崩溃恢复就要等半分钟。你的实际选择是在&amp;quot;误抢概率&amp;quot;和&amp;quot;恢复延迟&amp;quot;之间画一条线，没有银弹。&lt;/p&gt;&#xA;&lt;h3 id=&#34;12-症状二续期失败醒来已不是自己的锁&#34;&gt;&lt;a href=&#34;#12-%e7%97%87%e7%8a%b6%e4%ba%8c%e7%bb%ad%e6%9c%9f%e5%a4%b1%e8%b4%a5%e9%86%92%e6%9d%a5%e5%b7%b2%e4%b8%8d%e6%98%af%e8%87%aa%e5%b7%b1%e7%9a%84%e9%94%81&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1.2 症状二：续期失败，醒来已不是自己的锁&#xA;&lt;/h3&gt;&lt;p&gt;业务比 TTL 长怎么办？工业界标准做法是 &lt;strong&gt;watchdog 续期&lt;/strong&gt;（如 Java 的 Redisson 看门狗）：后台线程每隔 N 秒续期一次锁，让锁在业务完成前不会过期。下面用 Go goroutine 模拟同样逻辑。&lt;/p&gt;&#xA;&lt;p&gt;听起来挺好。但 watchdog 跟业务在同一个进程里。&lt;/p&gt;&#xA;&lt;p&gt;我做了个 STW 模拟。A SETNX TTL=3s，watchdog 间隔 1s。业务跑 2 秒后，整个进程被&amp;quot;冻结&amp;quot;5 秒，GC pause、虚拟机迁移、调度延迟都是常见的原因。watchdog 一起停了。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.00s][A] SETNX client_A → true (TTL=3s)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 1.00s][A.watchdog] EXPIRE → 续期成功？true&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 2.00s][A] ⚠️ 模拟 STW 5s 开始（业务 + watchdog 都被冻结）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 5.04s][B] SETNX client_B → true  ✅ 抢到锁&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 7.00s][A] STW 结束，业务继续&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 7.00s][A] GET 锁 owner = &amp;#34;client_B&amp;#34;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 7.00s][A] ⚠️ 锁已易主——但仅在主动 GET 时才察觉&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;T+5.04s B 抢到了锁。T+7s A 苏醒。如果业务代码没做 owner 校验就直接 DEL+扣库存，一次双持事故就这样发生了。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/ch1-e2-stw-dual-hold.png&#34; alt=&#34;双持时序&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这里的关键不是&amp;quot;watchdog 没用&amp;quot;，在没有长时间进程冻结的常规场景下，watchdog 确实能兜住。&lt;strong&gt;真正没解的是跨 TTL 的 STW&lt;/strong&gt;。如果你业务最长会卡多久心里没数，TTL 选多大都是赌博。&lt;/p&gt;&#xA;&lt;p&gt;工程上能做的兜底是：每次写之前 GET owner 字段，确认还是自己再继续；用 Lua 脚本做 GET+DEL 原子比较。但这只是把竞态推后，没消灭。A 真的发出 GET 之前，那一小段窗口里还是可能扣到 B 的库存上。&lt;/p&gt;&#xA;&lt;p&gt;我在线上见过的处理是这样：把锁拿来当&lt;strong&gt;意图协调&lt;/strong&gt;而不是&lt;strong&gt;互斥保护&lt;/strong&gt;——锁只决定&amp;quot;谁来发起这次扣减&amp;quot;，真正防扣超的是数据库的乐观锁（version 字段或 SQL 条件更新）。Redis 锁丢一次，最坏是两个 worker 同时尝试，但数据库会让其中一个失败。这种&amp;quot;锁 + 强幂等&amp;quot;的组合，是大部分用 Redis 锁的业务实际在用的兜底。&lt;/p&gt;&#xA;&lt;h3 id=&#34;13-症状三主从切换的瞬间锁就消失了&#34;&gt;&lt;a href=&#34;#13-%e7%97%87%e7%8a%b6%e4%b8%89%e4%b8%bb%e4%bb%8e%e5%88%87%e6%8d%a2%e7%9a%84%e7%9e%ac%e9%97%b4%e9%94%81%e5%b0%b1%e6%b6%88%e5%a4%b1%e4%ba%86&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1.3 症状三：主从切换的瞬间，锁就消失了&#xA;&lt;/h3&gt;&lt;p&gt;前两个场景至少 Redis 自己没&amp;quot;撒谎&amp;quot;。第三个场景更刺眼：&lt;strong&gt;Redis 的复制是异步的&lt;/strong&gt;。主库写入成功后，可能还没同步到从库，主库就挂了。&lt;/p&gt;&#xA;&lt;p&gt;我在本机用 Docker 起了一对主从，跑了一个最坏时序：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.00s] === E3 主从切换丢锁实验开始 ===&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.01s] ✅ 主从关系建立：master=127.0.0.1:6379, replica=127.0.0.1:6380&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.01s] 🔌 replica 提前断开复制（模拟最坏复制延迟）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.01s][A] master.SETNX → true (TTL=30s)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.95s] 💥 旧 master 已关闭&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 1.16s] ⚠️  新 master 上 GET order:1003:lock → 不存在（锁丢了！）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 1.16s][B] new_master.SETNX → true&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A 在 master 上 SETNX 成功。在复制还没到达 replica 时，master 挂了。哨兵把 replica 提升为新 master。客户端 B 连上新 master，对同一个 key 做 SETNX，&lt;strong&gt;也成功了&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;这一刻，同一把锁被两个客户端持有。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/ch1-e3-failover-lock-lost.png&#34; alt=&#34;主从切换丢锁&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;为了让本机能稳定复现这个时序，实验里我让 replica 提前断开复制。&lt;strong&gt;真实事故里这个延迟来自网络抖动、replica 还在做 RDB 全量同步、跨机房链路阻塞&lt;/strong&gt;。延迟具体是 100 毫秒还是 5 秒不重要，重要的是它存在。&lt;/p&gt;&#xA;&lt;p&gt;Redis 异步复制优先保证低延迟和高可用，以写入一致性为代价。如果你在 redis.conf 里配 &lt;code&gt;min-replicas-to-write 1&lt;/code&gt;，写入会要求至少 1 个 replica 满足滞后条件才接受。这能&lt;strong&gt;缩小&lt;/strong&gt;丢锁的窗口（Redis 文档的原话是&amp;quot;best effort data safety mechanism&amp;quot;），但不能彻底消除——复制仍是异步的。代价是可用性下降（replica 不可达时拒绝写入），窗口缩小了，没消除。&lt;/p&gt;&#xA;&lt;h3 id=&#34;14-三个症状到这里就够了&#34;&gt;&lt;a href=&#34;#14-%e4%b8%89%e4%b8%aa%e7%97%87%e7%8a%b6%e5%88%b0%e8%bf%99%e9%87%8c%e5%b0%b1%e5%a4%9f%e4%ba%86&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1.4 三个症状到这里就够了&#xA;&lt;/h3&gt;&lt;p&gt;读到这你会发现一件事：&lt;strong&gt;这三个场景都不是 Redis 实现得不好，它们是 SETNX 这个抽象本身的边界&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;根因在更深一层。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;2-病理分析三种症状的共同根因&#34;&gt;&lt;a href=&#34;#2-%e7%97%85%e7%90%86%e5%88%86%e6%9e%90%e4%b8%89%e7%a7%8d%e7%97%87%e7%8a%b6%e7%9a%84%e5%85%b1%e5%90%8c%e6%a0%b9%e5%9b%a0&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2. 病理分析：三种症状的共同根因&#xA;&lt;/h2&gt;&lt;p&gt;把三个症状摆开看，它们看起来很不一样：一个是进程死了、一个是进程没死但卡了、一个是 Redis 自己换了主。但我发现，它们在更深一层是&lt;strong&gt;同一类问题&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;我归出三个根因。&lt;/p&gt;&#xA;&lt;h3 id=&#34;21-根因一异步是性能的代价也是一致性的漏洞&#34;&gt;&lt;a href=&#34;#21-%e6%a0%b9%e5%9b%a0%e4%b8%80%e5%bc%82%e6%ad%a5%e6%98%af%e6%80%a7%e8%83%bd%e7%9a%84%e4%bb%a3%e4%bb%b7%e4%b9%9f%e6%98%af%e4%b8%80%e8%87%b4%e6%80%a7%e7%9a%84%e6%bc%8f%e6%b4%9e&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2.1 根因一：异步是性能的代价，也是一致性的漏洞&#xA;&lt;/h3&gt;&lt;p&gt;Redis 主从是异步复制；watchdog 续期是异步心跳；客户端连接 Redis 也是异步往返。&lt;strong&gt;异步说白了就是&amp;quot;我假设另一边在线，但我不真知道&amp;quot;&lt;/strong&gt;。你写入主库假设它会同步到 replica，但你不会等同步完成；watchdog 假设业务还在跑，但 STW 期间它什么都不知道。&lt;/p&gt;&#xA;&lt;p&gt;E3 是异步复制踢到的雷。E2 是异步心跳踢到的雷。E1 也算——客户端断了，server 端没 hook 去知道。&lt;/p&gt;&#xA;&lt;p&gt;你想消灭异步？可以，上 raft，写入要求过半节点 ack 才返回。代价是 QPS 下降，因为每次写都要走一轮共识。&lt;/p&gt;&#xA;&lt;h3 id=&#34;22-根因二时钟假设比你想象的更脆&#34;&gt;&lt;a href=&#34;#22-%e6%a0%b9%e5%9b%a0%e4%ba%8c%e6%97%b6%e9%92%9f%e5%81%87%e8%ae%be%e6%af%94%e4%bd%a0%e6%83%b3%e8%b1%a1%e7%9a%84%e6%9b%b4%e8%84%86&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2.2 根因二：时钟假设比你想象的更脆&#xA;&lt;/h3&gt;&lt;p&gt;Redis 锁的 TTL 是基于 server 端时钟。Redlock 算法依赖五个 Redis 节点的时钟&amp;quot;基本一致&amp;quot;。Kleppmann 那篇著名的文章里反复强调，&lt;strong&gt;分布式系统中时钟不可靠&lt;/strong&gt;。NTP 调整、虚拟机时钟漂移、容器宿主机迁移，都会让&amp;quot;server 端的 1 秒&amp;quot;和&amp;quot;另一个 server 端的 1 秒&amp;quot;不再相等。&lt;/p&gt;&#xA;&lt;p&gt;E2 的本质是进程暂停（process pause）：客户端被冻结，无法执行任何代码（包括续期），而 Redis 服务端时钟正常走，TTL 照常过期。这不是时钟漂移，是进程对时间流逝完全无感知。时钟假设问题更多暴露在 Redlock 场景——多个 Redis 节点间的时钟漂移会直接破坏&amp;quot;多数派有效时间计算&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;时钟假设藏在分布式锁的每一寸里：TTL、心跳、过期判定、共识 leader election 都依赖时钟。差别只是&lt;strong&gt;有些方案让时钟假设暴露在皮肤上，有些把它藏在皮下&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;23-根因三进程状态对-server-是黑盒&#34;&gt;&lt;a href=&#34;#23-%e6%a0%b9%e5%9b%a0%e4%b8%89%e8%bf%9b%e7%a8%8b%e7%8a%b6%e6%80%81%e5%af%b9-server-%e6%98%af%e9%bb%91%e7%9b%92&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2.3 根因三：进程状态对 server 是黑盒&#xA;&lt;/h3&gt;&lt;p&gt;Redis 不知道你的进程是不是还在跑。它只能看 TCP 连接是不是断了，但 TCP 连接断的判定依赖 keepalive 或主动 RST，可能要等几十秒。&lt;/p&gt;&#xA;&lt;p&gt;E1 里 A 进程被 kill -9 之后，TCP 连接&amp;quot;看起来还在&amp;quot;，Redis 没法及时知道，要等 TTL 自己过期。&lt;/p&gt;&#xA;&lt;p&gt;这是为什么 etcd 和 ZK 走了完全不同的路。它们让 server 端主动维护&amp;quot;客户端是不是活着&amp;quot;的状态，用 lease（etcd）或 session（ZK）。客户端定期发心跳，断了 server 主动清理。&lt;strong&gt;这把&amp;quot;我假设你在线&amp;quot;换成了&amp;quot;我看着你呢&amp;quot;&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;24-看出来了吗&#34;&gt;&lt;a href=&#34;#24-%e7%9c%8b%e5%87%ba%e6%9d%a5%e4%ba%86%e5%90%97&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2.4 看出来了吗&#xA;&lt;/h3&gt;&lt;p&gt;E1/E2/E3 不是 Redis 的&amp;quot;三个 bug&amp;quot;，是 SETNX 这个 API 的三个体质风险。要选别的方案，&lt;strong&gt;关键在于它们针对这三个根因做了不同的设计&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/ch2-root-cause-map.png&#34; alt=&#34;根因映射&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;3-处方对照etcd-与-zk-是怎么针对根因设计的&#34;&gt;&lt;a href=&#34;#3-%e5%a4%84%e6%96%b9%e5%af%b9%e7%85%a7etcd-%e4%b8%8e-zk-%e6%98%af%e6%80%8e%e4%b9%88%e9%92%88%e5%af%b9%e6%a0%b9%e5%9b%a0%e8%ae%be%e8%ae%a1%e7%9a%84&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;3. 处方对照：etcd 与 ZK 是怎么针对根因设计的&#xA;&lt;/h2&gt;&lt;p&gt;我不打算讲&amp;quot;etcd 是什么、ZK 是什么&amp;quot;，这两个概念的入门资料满地都是。我想讲的是：&lt;strong&gt;它们针对症状一、二、三分别用了什么机制&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;31-etcdlease--txncas--watch&#34;&gt;&lt;a href=&#34;#31-etcdlease--txncas--watch&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;3.1 etcd：lease + Txn(CAS) + Watch&#xA;&lt;/h3&gt;&lt;p&gt;这三件套各自对应一个根因。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Lease 解决&amp;quot;进程状态对 server 是黑盒&amp;quot;&lt;/strong&gt;。客户端 Grant 一个 lease（带 TTL），KeepAlive 定期续期。客户端断了，server 自己 revoke。我跑了个 demo：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 1.01s] === 场景 2：B 获锁后崩溃 → server 自动 revoke ===&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 1.01s][B] Grant lease (id=18f9e7d22bbd80b, ttl=3s)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 1.01s][B] Txn(CAS) Put → succeeded=true（不调 KeepAlive，模拟崩溃）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 4.50s][C] Watch 到 key 删除，立即 Txn(CAS) → 拿到锁，等待 3.489s&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;B 不调 KeepAlive 模拟崩溃，3.489 秒（接近 TTL=3s，多出的约 0.5s 来自 lease 过期检查间隔 + raft 提交延迟）后 server 自动 revoke 了 lease，绑定的 key 自动消失。&lt;strong&gt;这里和 Redis 的关键差别&lt;/strong&gt;：Redis 是&amp;quot;key 自己 TTL 过期&amp;quot;，etcd 是 lease 到期后 revoke 走 raft 共识——集群一致删除，不会因主从切换丢失过期判定。&lt;/p&gt;&#xA;&lt;p&gt;原子互斥靠 Txn(CAS)：&lt;code&gt;If(CreateRevision == 0).Then(Put)&lt;/code&gt;——只在 key 不存在时写入。这跟 SETNX 表面上是一回事，但底层不同：etcd 的 Txn 走 raft，写入需要多数派节点 ack 才算成功。&lt;/p&gt;&#xA;&lt;p&gt;剩下的问题是抢占公平——etcd &lt;code&gt;concurrency&lt;/code&gt; 包的实现是：所有候选者在同一 prefix 下创建带 lease 的 key，按 CreateRevision 排序，最小者持锁；其余 watch 自己的前驱 key 删除事件——与 ZK watch 前驱等价，无惊群。&lt;/p&gt;&#xA;&lt;p&gt;需要明确：etcd 的 lease KeepAlive 同样跑在客户端进程里——STW 时它一样停，lease 照样会被 server revoke。etcd 对 E2 的防御不是&amp;quot;永远不丢锁&amp;quot;，而是 revision 做 fencing：即使锁被 revoke、新客户端拿到锁，旧客户端苏醒后的写入会被存储层拒绝（revision 已过期）。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;对应根因二（异步/复制）&lt;/strong&gt;：raft 是多数派共识，写入过半才返回。&lt;strong&gt;没有&amp;quot;复制还没到达 replica，master 就挂了&amp;quot;这个窗口&lt;/strong&gt;——挂了的 master 它的写入也没&amp;quot;成功&amp;quot;，新 leader 不会有这条幻数据。E3 在 etcd 里不存在。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/ch3-etcd-mechanism.png&#34; alt=&#34;etcd 机制&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;32-zk临时顺序节点--watch-前驱&#34;&gt;&lt;a href=&#34;#32-zk%e4%b8%b4%e6%97%b6%e9%a1%ba%e5%ba%8f%e8%8a%82%e7%82%b9--watch-%e5%89%8d%e9%a9%b1&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;3.2 ZK：临时顺序节点 + Watch 前驱&#xA;&lt;/h3&gt;&lt;p&gt;ZK 的设计哲学跟 etcd 类似，但有两个有趣的差别。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;EPHEMERAL 节点 = etcd 的 lease&lt;/strong&gt;。Session 断了，节点自动删，锁自动释放。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;SEQUENTIAL（顺序）+ Watch 前驱 = 公平 + 抗惊群&lt;/strong&gt;。所有候选者在同一路径下创建顺序节点，ZK 给每个附加单调递增序号；序号最小的是持锁者，其他人 Watch 自己&lt;strong&gt;前一个&lt;/strong&gt;节点的删除事件。这有两个好处：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;公平&lt;/strong&gt;：按到达顺序排队，不是先轮询到的赢&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;无惊群&lt;/strong&gt;：只 watch 前驱，持锁者释放只唤醒一个后继&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;etcd &lt;code&gt;concurrency&lt;/code&gt; 包已内置类似 ZK 的 watch 前驱机制（按 CreateRevision 排序），两者在公平性和抗惊群上实质等价。ZK 的独特优势是 SEQUENTIAL 序号天然可作 fencing token（单调递增，无需额外映射）。&lt;/p&gt;&#xA;&lt;p&gt;我跑了个三 client demo：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.04s][C1] 创建顺序节点 lock-0000000000&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.04s][C1] 🟢 我是序号最小（lock-0000000000）→ 持锁&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.23s][C2] 创建顺序节点 lock-0000000001&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.23s][C2] Watch 前驱节点 lock-0000000000&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.43s][C3] 创建顺序节点 lock-0000000002&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.43s][C3] Watch 前驱节点 lock-0000000001&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.74s][C2] 收到前驱 lock-0000000000 删除事件，重新检查序列&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.74s][C1] 🔓 已释放锁（删除 lock-0000000000）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 0.74s][C2] 🟢 我是序号最小（lock-0000000001）→ 持锁&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 1.65s][C3] 收到前驱 lock-0000000001 删除事件&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[T+ 1.65s][C3] 🟢 我是序号最小（lock-0000000002）→ 持锁&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;三个客户端按顺序拿锁。任何一个中途 session 断，ZK 删它的 ephemeral 节点，后继自动顶上。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;SEQUENTIAL 序号本身就是 fencing token&lt;/strong&gt;：单调递增，存储层可以拒绝旧 token。这是 Kleppmann 在那篇文章里推崇 ZK 的关键原因——fencing 不是另外加的一层，是协议自带的。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/ch3-zk-chain.png&#34; alt=&#34;ZK 链路&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;33-这两个方案不是没有代价&#34;&gt;&lt;a href=&#34;#33-%e8%bf%99%e4%b8%a4%e4%b8%aa%e6%96%b9%e6%a1%88%e4%b8%8d%e6%98%af%e6%b2%a1%e6%9c%89%e4%bb%a3%e4%bb%b7&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;3.3 这两个方案不是没有代价&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;维度&lt;/th&gt;&#xA;          &lt;th&gt;etcd&lt;/th&gt;&#xA;          &lt;th&gt;ZK&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;写入 QPS（本机串行）&lt;/td&gt;&#xA;          &lt;td&gt;896（实测）&lt;/td&gt;&#xA;          &lt;td&gt;未实测（参考：ZAB 共识 + JVM 开销，预期略低于 etcd）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;客户端语言生态&lt;/td&gt;&#xA;          &lt;td&gt;Go 原生，gRPC 多语言&lt;/td&gt;&#xA;          &lt;td&gt;Java（Curator）成熟，其他语言一般&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;运维心智&lt;/td&gt;&#xA;          &lt;td&gt;raft 集群 + compact + quota&lt;/td&gt;&#xA;          &lt;td&gt;JVM 调优 + observer + ZAB&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Fencing token&lt;/td&gt;&#xA;          &lt;td&gt;revision 号&lt;/td&gt;&#xA;          &lt;td&gt;sequential 序号&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;两者都用强一致协议（raft/ZAB），都用 server 端心跳替代客户端 TTL，都内置抢占公平和 fencing。但&lt;strong&gt;都比 Redis 慢&lt;/strong&gt;：本机 benchmark 测下来 etcd 比 Redis 慢 2.21 倍（单 client 串行 round-trip：Redis 1978 QPS，etcd 896 QPS）。&lt;/p&gt;&#xA;&lt;p&gt;慢这件事不能怪 etcd 写得差，是共识协议本身有代价：每次写要等多数派节点 ack。这是它能消除症状三的代价。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;4-处方判定qps--一致性--运维成本三维决策树&#34;&gt;&lt;a href=&#34;#4-%e5%a4%84%e6%96%b9%e5%88%a4%e5%ae%9aqps--%e4%b8%80%e8%87%b4%e6%80%a7--%e8%bf%90%e7%bb%b4%e6%88%90%e6%9c%ac%e4%b8%89%e7%bb%b4%e5%86%b3%e7%ad%96%e6%a0%91&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4. 处方判定：QPS × 一致性 × 运维成本三维决策树&#xA;&lt;/h2&gt;&lt;p&gt;到这里，三种方案的能力边界你应该清楚了。能力边界不等于决策。你的业务是什么决定了你该选谁。&lt;/p&gt;&#xA;&lt;p&gt;我把三个维度做成决策树，每条分叉对应一个真实约束。&lt;/p&gt;&#xA;&lt;h3 id=&#34;41-三维度的含义&#34;&gt;&lt;a href=&#34;#41-%e4%b8%89%e7%bb%b4%e5%ba%a6%e7%9a%84%e5%90%ab%e4%b9%89&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4.1 三维度的含义&#xA;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;QPS&lt;/strong&gt;：单位时间获锁/释放的次数。Redis 单实例本机实测 1978，etcd 896，比例 2.21x；并发吞吐差距更大（etcd 写入要走 raft 共识）。如果你的锁场景是&amp;quot;秒杀峰值 5000 QPS&amp;quot;，三方案都能接住；如果是&amp;quot;每秒 10 万次去重&amp;quot;，只有 Redis 撑得住。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;一致性&lt;/strong&gt;：失败一次的代价。&amp;ldquo;扣库存扣超&amp;quot;和 &amp;ldquo;缓存重建多算一次&amp;rdquo; 完全不是一个量级——前者三天事故复盘，后者用户都没察觉。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;运维成本&lt;/strong&gt;：你团队有没有 Java 同学维护 ZK？你有没有独立部署的 etcd 集群？时钟同步、跨机房链路，这些隐形成本不能忽略。&lt;/p&gt;&#xA;&lt;h3 id=&#34;42-决策树&#34;&gt;&lt;a href=&#34;#42-%e5%86%b3%e7%ad%96%e6%a0%91&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4.2 决策树&#xA;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;你需要分布式锁的目标是什么？&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;A. 互斥即可&lt;/strong&gt;（去重/限流/防重复点击）&#xA;→ Redis SETNX（主从 + watchdog），业务侧做幂等兜底&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;B. 必须保证不重复&lt;/strong&gt;（金融/库存/订单）&#xA;→ &lt;strong&gt;B1. QPS &amp;lt; 5k&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;团队有 etcd 运维经验 + 独立部署的 etcd 集群 → &lt;strong&gt;etcd&lt;/strong&gt;&lt;/li&gt;&#xA;&lt;li&gt;否则 → &lt;strong&gt;ZK&lt;/strong&gt;（如果有 Java 团队）/ &lt;strong&gt;etcd&lt;/strong&gt;（独立部署）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;→ &lt;strong&gt;B2. QPS &amp;gt; 5k&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;业务能拿 fencing token 在写入侧校验 → &lt;strong&gt;Redis SETNX + 业务 token&lt;/strong&gt;&lt;/li&gt;&#xA;&lt;li&gt;否则 → &lt;strong&gt;重新设计&lt;/strong&gt;（这种 QPS 下分布式锁未必是答案，考虑分片 / 无锁队列 / 乐观并发）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;⚠️ &lt;strong&gt;k8s 控制面的 etcd 不适合直接承载业务锁&lt;/strong&gt;——资源竞争会影响集群稳定性，且权限隔离不足。已有 etcd 指独立部署的集群。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/ch4-decision-tree.png&#34; alt=&#34;决策树&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;43-一个-worked-example电商超卖&#34;&gt;&lt;a href=&#34;#43-%e4%b8%80%e4%b8%aa-worked-example%e7%94%b5%e5%95%86%e8%b6%85%e5%8d%96&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4.3 一个 worked example：电商超卖&#xA;&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;业务&lt;/strong&gt;：库存扣减&#xA;&lt;strong&gt;QPS&lt;/strong&gt;：日常 500，秒杀峰值 5000&#xA;&lt;strong&gt;一致性要求&lt;/strong&gt;：库存不能扣超&#xA;&lt;strong&gt;已有基础设施&lt;/strong&gt;：独立 etcd 集群（团队有运维经验）+ Redis 主从&lt;/p&gt;&#xA;&lt;p&gt;走决策树：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;必须保证不重复 → 右分支&lt;/li&gt;&#xA;&lt;li&gt;QPS &amp;lt; 5k（峰值刚好踩线）→ 左分支&lt;/li&gt;&#xA;&lt;li&gt;团队有 etcd 运维经验 + 独立集群 → &lt;strong&gt;选 etcd&lt;/strong&gt;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;可能的反对意见：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&amp;ldquo;Redis + 业务幂等更轻&amp;rdquo;——可以。但业务幂等需要订单号或自然键去重，且要承认 E1/E2/E3 三场景的概率&lt;/li&gt;&#xA;&lt;li&gt;&amp;ldquo;etcd 只能 ~10k QPS&amp;rdquo;——确实有上限，但 5k 还在舒适区&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;44-反例什么时候选-redis-也是对的&#34;&gt;&lt;a href=&#34;#44-%e5%8f%8d%e4%be%8b%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e9%80%89-redis-%e4%b9%9f%e6%98%af%e5%af%b9%e7%9a%84&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4.4 反例：什么时候选 Redis 也是对的&#xA;&lt;/h3&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;场景&lt;/th&gt;&#xA;          &lt;th&gt;为什么 Redis 够&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;限流（每秒 N 次）&lt;/td&gt;&#xA;          &lt;td&gt;不需要严格互斥，丢一两次锁可以接受&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;缓存重建去重&lt;/td&gt;&#xA;          &lt;td&gt;多个 worker 重建结果一致，丢锁最多多重建一次&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;后台批处理调度&lt;/td&gt;&#xA;          &lt;td&gt;业务侧本身有任务 ID 去重&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;防止重复点击&lt;/td&gt;&#xA;          &lt;td&gt;短期 TTL，宁可松一点也别拖延用户响应&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&lt;strong&gt;核心判断&lt;/strong&gt;：失败一次的代价 vs 严格一致性的运维成本。失败的代价小，Redis 性价比最高。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/ch4-matrix-plus-quote.png&#34; alt=&#34;决策矩阵&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;5-医患交底redlock-论战与我不替你做选择&#34;&gt;&lt;a href=&#34;#5-%e5%8c%bb%e6%82%a3%e4%ba%a4%e5%ba%95redlock-%e8%ae%ba%e6%88%98%e4%b8%8e%e6%88%91%e4%b8%8d%e6%9b%bf%e4%bd%a0%e5%81%9a%e9%80%89%e6%8b%a9&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;5. 医患交底：Redlock 论战与&amp;quot;我不替你做选择&amp;rdquo;&#xA;&lt;/h2&gt;&lt;p&gt;不写完 Redlock 论战，这篇文章就缺了一块。但我不打算下结论。&lt;/p&gt;&#xA;&lt;p&gt;Redlock 的核心思路：部署 N 个独立 Redis 节点（无主从），客户端依次向每个节点 SETNX，超过半数成功且总耗时 &amp;lt; TTL 剩余时间，则认为获锁成功。antirez 设计它是为了绕过单点 Redis 的主从复制问题——用多数派代替主从。&lt;/p&gt;&#xA;&lt;p&gt;Kleppmann 在 2016 年那篇 &lt;em&gt;How to do distributed locking&lt;/em&gt; 里说：如果你需要&lt;strong&gt;正确性&lt;/strong&gt;，不要用 Redlock；如果你只要&lt;strong&gt;效率锁&lt;/strong&gt;，单节点 Redis 就够。他主张用 fencing token——单调递增 token 配合存储层拒绝旧 token，这才是真正的安全。&lt;/p&gt;&#xA;&lt;p&gt;antirez（Redis 作者）的反驳是：&lt;strong&gt;时钟跳变在实践中远小于锁 TTL&lt;/strong&gt;，工程上够用；fencing token 的论证更像学术分析而非工程现实——如果你已经有 fencing token 能用，那本身就不需要分布式锁了。&lt;/p&gt;&#xA;&lt;p&gt;我读过双方的原文，老实说没法当裁判。如果非要站队——我更认同 Kleppmann 的安全性分析框架，前四章的实验已经说明了原因。但 antirez 关于工程约束的反驳是真实的：不是每个团队都有能力落地 fencing token。两边的论据都成立：Kleppmann 在分析理论安全性的边界，antirez 在守护工程务实的可行性。截至 2026 年，双方核心分歧仍未解决。&lt;/p&gt;&#xA;&lt;p&gt;我能给你的是判断条件——&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;如果你的业务&amp;quot;丢一次锁会引来三天的事故复盘&amp;quot;，按 Kleppmann 的判断走——选 etcd 或 ZK，要 fencing&lt;/li&gt;&#xA;&lt;li&gt;如果你的业务&amp;quot;丢一次锁顶多多发一次通知&amp;quot;，按 antirez 的判断走——单 Redis 或 Redlock 都行&lt;/li&gt;&#xA;&lt;li&gt;如果你说不清自己在哪一档，&lt;strong&gt;先去想清楚业务&lt;/strong&gt;，再回来选锁&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;回到开头那个凌晨三点的告警。你的锁不是&amp;quot;要不要选最强的&amp;quot;，是&amp;quot;要不要先体检&amp;quot;。看清楚自己的业务能容忍什么，再决定开什么药。&lt;/p&gt;&#xA;&lt;p&gt;我不替你做这个选择。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/distributed-lock-selection/ch4-conclusion.png&#34; alt=&#34;先看清你的业务，再选你的锁&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;附录实验代码和原始数据&#34;&gt;&lt;a href=&#34;#%e9%99%84%e5%bd%95%e5%ae%9e%e9%aa%8c%e4%bb%a3%e7%a0%81%e5%92%8c%e5%8e%9f%e5%a7%8b%e6%95%b0%e6%8d%ae&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;附录：实验代码和原始数据&#xA;&lt;/h2&gt;&lt;p&gt;本文 6 组实验的代码和运行日志已开源：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;GitHub：&lt;a class=&#34;link&#34; href=&#34;https://github.com/wujiachen0727/zhiyulab-evidence/tree/main/distributed-lock-selection&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;zhiyulab-evidence/distributed-lock-selection&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;子目录&lt;/th&gt;&#xA;          &lt;th&gt;内容&lt;/th&gt;&#xA;          &lt;th&gt;对应正文&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;01-process-crash/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;进程崩溃后锁卡死直到 TTL 实验&lt;/td&gt;&#xA;          &lt;td&gt;§1.1 症状一&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;02-renewal-fail/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;STW 续期失败双持实验&lt;/td&gt;&#xA;          &lt;td&gt;§1.2 症状二&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;03-master-failover/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Redis 主从切换丢锁实验（docker-compose）&lt;/td&gt;&#xA;          &lt;td&gt;§1.3 症状三&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;04-etcd-lock/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;etcd lease+Txn(CAS) 锁实验&lt;/td&gt;&#xA;          &lt;td&gt;§3.1 etcd&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;05-zk-lock/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;ZK 临时顺序节点锁实验&lt;/td&gt;&#xA;          &lt;td&gt;§3.2 ZK&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;每个子目录都有独立 README，说明如何复现。二进制编译产物不入库，跑实验前自己 &lt;code&gt;go build&lt;/code&gt;。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;原文发布于 &lt;a class=&#34;link&#34; href=&#34;https://www.wujiachen.com.cn/posts/distributed-lock-selection&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;止语Lab&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;</description>
        </item></channel>
</rss>
