分布式协调原语:注册中心、ID生成、分布式锁
一致性的底层 + 三个最常用协调组件的技术对比
本主题的底层路线(万变不离其宗):
注册中心 / ID 生成 / 分布式锁 的底层 = 一致性协议(Raft / ZAB) + 心跳 / 租约(lease) + 订阅回调(watch) + 互斥原语 。
它们不是三种技术,而是「共识 + 租约」这套机制的三种应用:注册中心用「共识保证目录一致 + 租约判定存活 + watch 推送变更」;ID 生成用「共识分配 workerId + 本地时钟/序列保证趋势有序」;分布式锁用「共识/原子操作抢互斥权 + 租约防死锁 + fencing 防失效」。一句话记住 先把"共识 + 租约"想透,三个组件只是它长出的三张脸。
0. 读前必读:三个组件,其实是一套机制
你可能在各种框架文档里被"注册中心 / 雪花算法 / 分布式锁"这三个名字搞晕——它们看起来毫不相关。但底层它们共享同一套协调基础设施 :
注册中心 (ZooKeeper / Etcd / Nacos / Eureka / Consul):回答"现在谁是活着的、谁提供什么服务"。
ID 生成器 (Snowflake / Leaf / UidGenerator):回答"在分布式多机下,怎么给每个事件一个全局不重、最好还大致有序的编号"。
分布式锁 (Redis / ZK / Etcd / DB):回答"多机并发时,怎么保证同一时刻只有一个人能干某件事"。
本部分的认知顺序: 先打牢"一致性底层"(CAP、quorum、lease、watch、共识算法)→ 再看三个组件如何各自把这套机制实例化并做出取舍。读每一节都问自己一句:"它用的是共识、还是租约、还是互斥?在哪个点上做了取舍?"
一句话记住 注册中心、ID、锁不是三件事,而是"共识 + 租约"这一个内核被用在了"服务目录、时间编号、互斥权"三个场景里。
1. 一致性基础:CAP / BASE / quorum / lease / watch / 共识
所有协调组件都建立在对"一致性与可用性如何取舍"的理解上。这一节是地基,后面三节都会回头引用它。
1.1 CAP 与"三选二"的真相
CAP 定理:一个分布式数据系统 在网络分区(P,Partition) 发生时,一致性(C,Consistency) 和可用性(A,Availability) 无法同时保证,只能二选一。
C(一致性) :每次读取都能拿到"最新写入"的结果(线性一致 / 强一致)。所有副本立刻一致。
A(可用性) :每个非故障节点 收到的请求都必须在有限时间内返回(不管数据新不新)。
P(分区容忍) :系统允许节点间网络断连(消息丢、延迟),仍能继续工作。
真相:P 通常"不得不保"。 只要你的系统是分布式的、跨节点的,网络分区就不可避免(网线断了、交换机抽风、GC 停顿假死都是"分区"的表现)。所以现实里不是"三选二",而是:
保 P,然后在 C 和 A 之间取舍 。
CP :分区时宁可拒绝服务(不可用),也要保证一致。典型:ZooKeeper、Etcd、Consul。
AP :分区时宁可返回旧数据(不一致),也要保证可用。典型:Eureka、Nacos 的 Distro 模式。
C 一致性
A 可用性
P 分区容忍(现实里通常必须保)
CP 系统(保一致+分区)
ZooKeeper · Etcd
Consul(牺牲 A)
Nacos(AP + CP 双模型)
注册走 AP,配置走 CP
AP 系统(保可用+分区)
Eureka(牺牲 C)
Nacos Distro 模式
图 1:CAP 与主流注册中心定位。网络分区难免 → 保 P;CP 系统在分区时牺牲可用性保一致,AP 系统牺牲一致性保可用。Nacos 因"双模型"横跨两岸。
一句话记住 CAP 不是"三选二",而是"保 P,在 C 与 A 间二选一";CP=宁可错峰不可用,AP=宁可短暂不一致。
1.2 BASE 与最终一致
强一致(C)代价高(要多数派确认、要等复制)。很多场景并不需要"此刻绝对一致",只要"过一会儿大家都会一致" 就够了——这就是最终一致性 。
BASE 是与之对应的设计哲学(与 ACID 相对):
BA (Basically Available,基本可用):分区时允许降级(响应慢一点、部分功能关闭),但不整体挂掉。
S (Soft state,软状态):允许系统中间存在"不一致的中间态",状态可以暂时不固定。
E (Eventually consistent,最终一致):在没有新写入后,经过一段时间,所有副本都会收敛到一致。
直觉: AP 注册中心(如 Eureka)就是 BASE 的化身——节点下线后,别的节点可能短暂拿到旧列表(软状态),但心跳恢复 / 重新拉取后最终会一致。消息队列、缓存、DNS 也都是最终一致。取舍 用"短暂不一致"换"永远不掉线"。
1.3 quorum:N/2+1 多数派
在"多副本"系统里,怎么判断一个值"算数"?靠法定人数(quorum) 。设副本数 N,写法定数 W、读法定数 R。为保证读到的一定是"最新写过的",经典约束是:
W + R > N
最常用的是 W = R = N/2 + 1 (多数派)。意思是:写要被超过半数 的节点确认,读也要从超过半数 的节点读取并取最新——两者必然交集,所以读到的一定是含最新写的那一份。
例:N=5,则 W=3、R=3。写进 3 个、读问 3 个必撞上同一个人,最新值不会漏。
代价:N 越大,写延迟越高(要等多数派 ack)。这就是"一致性换性能"的具体体现。
一句话记住 quorum 的核心是"W+R>N"——让"写集合"和"读集合"必然相交,从而读到最新;多数派 N/2+1 是最常用的那一档。
1.4 lease 租约:带期限的授权
很多协调问题可归结为"谁有权做某事 "(谁是主、谁能拿锁、谁还活着)。最优雅的机制之一是租约(lease) 。
租约 = 一方给另一方一张"在 T 时间内有效的授权 "。持有者在 T 内可放心行使权利;T 到期前必须续约(renew) ,否则授权自动失效。
为什么需要 lease?缓解"时钟漂移"。 分布式里没有全局一致的时钟,你不能说"10:00:00 整所有人同时失效"。lease 把"失效判定"交给定时器(本地倒计时),而不是靠节点间对表。只要租约长度 ≫ 最大时钟误差 ,就不会出现"两个人都以为自己合法"的危险窗口。
心跳 vs 租约: 心跳是"我还活着"的主动上报;租约是"你被授权活到 T 时刻"的被动倒计。注册中心的"临时节点 / 续约 TTL"本质就是 lease。
典型应用: ZK 临时节点的会话超时、Etcd 的 lease TTL、Redis 锁的过期时间、服务实例的"健康租约"。
一句话记住 lease 是"带期限、需续约的授权";它用本地倒计时绕开了"分布式没有统一时钟"的难题,是防死锁、判存活的万能钥匙。
1.5 watch / 回调订阅
协调系统的第二大能力是"状态变了通知我 ",而不是让我不停去问(轮询)。这叫 watch / 订阅-推送 。
ZooKeeper watch: 一次性触发器。你 watch 一个 znode,它一变就推一次事件,然后 watch 消失,需重新注册(否则会漏中间的变化)。这是一个经典"坑"。
Etcd watch: 支持持续监听 + 从某个 revision 开始回放 ,天然避免漏事件;还能 watch 一段前缀(目录级)。
推送 vs 拉取: watch 是"推"(服务端主动);客户端也可定期拉全量兜底(Eureka 客户端就是拉取 + 缓存)。
一句话记住 watch 让"配置/服务列表变化"从"客户端轮询"变成"服务端推送";但务必确认它是"一次性"还是"可回放",否则会漏事件。
1.6 共识简述:Raft / ZAB / Paxos
"多个节点对某个值的顺序达成一致"叫共识(consensus) 。它是 CP 系统的引擎。
Raft(Etcd / Consul / Nacos-CP 用)
Raft 把共识拆成三个易理解的子问题:
领导选举(Leader Election): 节点有 Follower / Candidate / Leader 三种角色。Follower 收不到 Leader 心跳超时 → 变 Candidate,投自己一票并要别人投票;拿到多数派 票 → 当 Leader。每届叫一个 Term(任期) ,Term 单调递增,防止旧 Leader 乱发号。
日志复制(Log Replication): 所有写先到 Leader;Leader 把命令作为"日志条目"追加,并复制到 Follower;收到多数派 ack 后,Leader 把它标记为已提交(committed) ,再应用到状态机,并通知 Follower 提交。
安全性(Safety): 选举限制——只有"日志至少和多数派一样新"的节点才能当选;已提交的条目不会丢、不会改。这保证了线性一致。
ZAB(ZooKeeper 用)
ZAB(ZooKeeper Atomic Broadcast)是给 ZK 定制的原子广播协议,思想和 Raft 同类:也有 Leader、也有"事务提案 → 多数派 ack → 提交"。区别在于 ZAB 强调事务顺序的全局一致 (ZXID 单调)和"崩溃恢复后保证已提交事务不丢"。简单说:Raft 是通用共识,ZAB 是 ZK 的专用共识 ,两者你中有我。
Paxos(思想源头)
Paxos 是共识理论的"祖师爷",证明最难懂。它的核心思想一句话:" proposer 提编号,acceptor 只接受编号更大的提案;多数派确认即达成" 。Raft / ZAB 都可看作"让 Paxos 好实现、好理解"的工程化版本。学习者不必死磕证明,记住"多数派 + 单调编号"即可。
上:Leader 选举(Term 单调递增,多数派获胜)
Follower
收不到心跳
超时 → 选举
Candidate
Term+1,自投
RequestVote
Leader
多数派票
发心跳维持
Follower B / C 收到 RequestVote → 投出自己的一票(每 Term 一票)
安全约束:只有"日志足够新"的节点才能当选;已提交条目绝不丢失、绝不回滚 → 线性一致。
下:日志复制(写先到 Leader → 多数派 ack → 提交)
Client
写请求
Leader
追加日志
Follower1
ack
Follower2
ack
① Leader 收到写,追加未提交日志条目(带递增 index / Term)。
② 并行复制到 Follower,收到「多数派 ack」。
③ Leader 提交(apply 到状态机)并通知 Follower 提交 → 此时对外可见,且永不回滚。
图 2:Raft 两大流程。上:选举靠"多数派票 + 单调 Term",且只有日志够新的节点能当选(安全性);下:写要经过"追加 → 多数派复制 → 提交"三段,保证线性一致。
一句话记住 共识的本质 = "选一个说话算数的 Leader + 用多数派确认保证日志顺序不乱 + 用单调任期/编号挡住旧节点捣乱"。
2. 注册中心:把"共识 + 租约"用成了服务目录
2.1 注册中心到底在协调什么
微服务里,服务提供者 (Server)上线要"报名"、下线要"销户",服务消费者 (Client)要知道"现在谁能调用"。注册中心要解决的三个底层问题,恰好对应三个原语:
要解决的问题 底层原语 说明
目录在哪、长啥样 共识(CP)或 最终一致(AP) 谁来写、写不写在多数派确认的地方
实例是否还活着 租约 / 心跳(lease) 临时节点 TTL / 心跳续约,超时即剔除
列表变了怎么知道 watch(订阅推送) 服务端主动推变更,省去轮询
Provider A
注册 + 心跳(lease)
Consumer B
watch 订阅
注册中心集群
共识同步目录
lease 判存活
watch 推送
Leader 写入口
Follower ×N
共识复制
(CP 系统)
变更 → 通过 watch 主动推给 B(而非 B 轮询)
图 3:注册中心生命周期。Provider 以租约方式注册/续约;集群用共识保持目录一致;Consumer 通过 watch 被动接收变更推送。
2.2 ZooKeeper(ZAB + 临时节点 + watch)
一致性: CP,基于 ZAB 协议(类 Raft 的原子广播)。写必须经 Leader,多数派 ack 才提交。写性能受限于选主节点 ,且集群写吞吐随节点数上升反而下降(要同步更多人)。
数据模型: 层级化的 znode 树 。两种关键节点:
持久节点: 显式删除才消失,存"配置/目录"。
临时节点(Ephemeral): 绑定客户端会话 ,会话断开(心跳/租约超时)即自动删除——天然实现"实例下线自动摘除"。
watch: 一次性触发器(见 §1.5),适合"节点增删改"事件。
典型用法: 服务注册(临时节点)、配置中心、分布式锁(临时顺序节点,§4.3)、选主。
取舍 ZK 强一致、可靠、功能全,但写受限单主、运维偏重(要奇数节点、要小心 znode 数量),不适合超高频写或海量 key。
2.3 Etcd(Raft + lease + watch)
一致性: CP,基于 Raft (Go 实现,参考 JPaxos 思路)。和 ZK 一样写走 Leader + 多数派。
数据模型: 扁平 key-value ,但 key 有序、可按前缀 组织成目录;带 revision(全局单调递增版本号) ——这是它的杀手锏。
lease: 原生 lease 原语,key 绑定 lease TTL,可批量续约;超时自动删。
watch: 支持持续监听 + 从指定 revision 回放 ,不会漏事件;还能 watch 前缀。比 ZK 的"一次性 watch"更省心。
接口: gRPC / HTTP(v3 版),云原生标配(Kubernetes 的后端就是 Etcd)。
一句话记住 Etcd = "Raft 保证一致 + revision 做版本 + lease 判存活 + watch 可回放";K8s 的选择,云原生第一公民。
2.4 Nacos(AP Distro + CP Raft 双模型)
Nacos 的聪明之处在于"一套产品,两种一致性按需切换" :
服务注册 / 发现(AP): 默认走 Distro 协议 ——去中心化、每个节点负责一部分数据的最终一致 复制;节点间 gossip 同步。可用性极高,适合"服务列表短暂不一致无伤大雅"的场景。
配置管理(CP): 走 Raft 保证配置强一致——配置错了可是大事,必须"看到的就是确定的"。
合一优势: 注册中心 + 配置中心一个组件搞定,运维成本低。
取舍 Nacos 用"注册 AP、配置 CP"的拆分,兼顾了"服务发现要永远在线"和"配置要绝对一致"两套诉求。代价是内部两套复制机制、理解成本略高。
2.5 Eureka(AP + 心跳 + 自我保护)
一致性: 纯 AP 。没有共识算法,节点间异步复制 ,允许短暂不一致。
健康判定: 客户端心跳 上报;服务端自我保护机制(Self-Preservation) ——当短时间大量实例心跳丢失(很可能是网络分区,而非真挂了)时,不急着全剔除 ,宁可保留旧列表,避免"雪崩式误删"。
客户端缓存: Consumer 本地缓存服务列表,即使注册中心全挂,也能用旧列表继续调用(牺牲"新"换"活")。
为什么 Eureka 敢 AP? 服务发现"偶尔拿到一个已下线实例"的后果,通常只是一次重试;而"注册中心在分区时拒绝所有发现请求"的后果是整个系统瘫痪。所以 Netflix 选择可用性优先。这也是 CAP 取舍的教科书案例。
2.6 Consul(Raft + gossip + 多数据中心)
一致性: CP,基于 Raft 管内部状态;外加 gossip 协议 做成员管理与多数据中心间的信息传播(gossip 是"随机告诉邻居、邻居再扩散"的 epidemics 式传播,最终全员知道)。
健康检查: 内置多种健康探测(HTTP / TCP / 脚本),比纯心跳更丰富。
多数据中心: 原生支持跨 DC,每个 DC 内部 Raft,DC 间 gossip 同步——对"全球部署"友好。
多合一: 服务发现 + KV 配置 + 健康检查 + 多数据中心,开箱即用。
2.7 横向对比表与选型
组件 一致性 底层协议 健康判定 推送方式 性能/特点 典型场景
ZooKeeper CP ZAB 临时节点(会话/租约) watch(一次性) 可靠但写受限单主;运维重 强一致注册/锁/选主/配置
Etcd CP Raft lease TTL watch(可回放) 云原生、gRPC、revision 强 K8s 生态、配置、锁
Nacos AP+CP 双 Distro / Raft 心跳 + 探活 推送 + 拉取 注册配置合一、易运维 Spring Cloud 微服务体系
Eureka AP 异步复制 心跳 + 自我保护 客户端拉取+缓存 高可用、容忍短暂不一致 纯服务发现、容错优先
Consul CP Raft + gossip 丰富健康检查 watch / 阻塞查询 原生多数据中心、多合一 多 DC、基础设施一体
一句话记住 选注册中心先问"你能接受短暂不一致吗":能 → Eureka/Nacos(AP);不能、且要强一致 → ZK/Etcd/Consul(CP);想注册配置一起管 → Nacos;要跨数据中心 → Consul。
3. 序列号 / ID 生成器:给每件事一个全局有序的"身份证"
分布式下,多机各自自增会撞号。ID 生成器的核心诉求:全局唯一 、尽量趋势递增 (让数据库索引更友好)、高性能 、高可用 。下面按"底层机制"从弱到强对比。
3.1 UUID 与数据库自增
UUID: 本地生成、无中心、高性能;但完全无序、字符串长、作为主键会让 B+ 树索引频繁分裂 (插入随机,页分裂严重,性能差)。适合"不需要排序、只要唯一"的场景(如traceId)。
数据库自增(auto_increment): 简单、绝对有序;但单点瓶颈、扩容(分库分表)时步长难调、有性能上限 。在分布式里基本被淘汰为"单机兜底"。
UUID 当主键是"索引杀手":InnoDB 主键即聚簇索引,无序插入导致页分裂 + 内存碎片,写入放大明显。
3.2 号段模式(Leaf-segment)
思路:数据库只负责"批发号段",应用内存"零售发号" 。
DB 里存 max_id, step;应用去取号段 [max_id+1, max_id+step],并把 max_id 更新为 max_id+step。
应用拿到号段后在本地内存原子自增 发号,完全不碰 DB,性能极高。
双 buffer 预取: 当前号段用到一定比例(如 10%)时,后台线程异步去取下一个号段 放进备用 buffer;当前号段耗尽直接切到备用,无发号停顿 。
号段1: [1, 1000] ← 当前在发(用到10%触发预取)
号段2: [1001, 2000] ← 后台已取好,随时顶上
取舍 号段模式强在"DB 压力小、发号极快、可独立部署";弱点是 ID 只是'号段内有序' ,跨号段不保证严格递增(重启可能跳号),且强依赖 DB 可用性(号段发完又没预取成功会短暂阻塞)。
3.3 Snowflake 与时钟回拨
Twitter 提出的经典时间戳派 方案,64 位整数,本地生成、无需中心、趋势递增。
1位
符号
41位 时间戳(毫秒,相对某纪元)
≈69年
10位 机器ID
5机+5机房
12位 序列
每毫秒4096
高位是时间 → 整体趋势递增;同毫秒内靠序列号区分
图 4:Snowflake 64 位结构。符号位恒 0;41 位毫秒时间戳保证"时间越大 ID 越大";10 位机器 ID 区分不同节点;12 位序列号在同一毫秒内自增。
为什么趋势递增: 高位是时间,时间单调递增 → 整体 ID 随生成时间递增,对索引友好。但不是严格连续 (不同机器、不同毫秒会跳)。
时钟回拨(核心坑): 如果机器时钟因为 NTP 校准往回跳 ,可能生成和"上一毫秒已发过的"相同的 ID → 重复 。这是 Snowflake 最著名的致命弱点。
workerId 分配: 10 位机器 ID 不能重复。常见分配:手动配置 / 借 ZooKeeper 或 DB 申请 / 启动时报到抢号 。分配错了会导致"不同节点用同一 workerId + 同一毫秒"撞号。
时钟回拨怎么处理? ① 小回拨(如<5ms):等时间追平再发;② 大回拨:抛异常或切换备用 workerId / 扩展位借用;③ 根本上——用 Leaf-snowflake(见下)把 workerId 上收托管,并缓存近期时间戳做检测。
3.4 Redis incr 与 Leaf-snowflake / UidGenerator
Redis incr: 用 INCR 原子自增拿号,简单、快;但强依赖 Redis 可用 ,且是集中式(所有节点抢同一 key,有网络往返,吞吐受限),扩展开销大。本质是"把 DB 自增换成 Redis 自增"。
Leaf-snowflake(美团): 在 Snowflake 上补两件事——① workerId 由 Leaf 服务统一从 DB / ZK 分配 (解决手动配错);② 缓存最近发号的时间戳 ,检测到时钟回拨时报警 + 拒绝发号 (而非生成重复)。
百度 UidGenerator: Snowflake 变体,把 64 位重新切分(如 28 位秒级时间 + 22 位 workId + 13 位序列),并用"号段方式发 machineId" (启动时用数据库号段申请一个 workerId 区间),解决 workerId 分配与回拨保护。
一句话记住 Snowflake 用"时间高位"做到趋势递增、本地生成;它的两个阿喀琉斯之踵是"时钟回拨"和"workerId 分配",Leaf / UidGenerator 正是为补这两洞而生。
3.5 横向对比
方案 趋势递增 有序性 性能 可用性 独立部署 弱点
UUID 否 无序 极高(本地) 极高 是 索引不友好
DB 自增 是 严格 低(单点) 依赖DB 否 单点/扩容难
Leaf-segment 段内 段内有序 极高 依赖DB(可降级) 是 跨段不连续/依赖DB
Snowflake 是 趋势 极高(本地) 极高 是 时钟回拨/workerId分配
Redis incr 是 严格 中(网络) 依赖Redis 否 集中瓶颈
Leaf-snowflake 是 趋势 极高 极高 是 依赖Leaf服务
取舍 要"严格连续 + 不怕中心"→ DB / Redis;要"高性能 + 趋势递增 + 去中心"→ Snowflake 系;要"高吞吐发号 + 可运维"→ Leaf-segment / Leaf-snowflake。
4. 分布式锁:把"互斥"这件事做对
4.1 锁必须满足的五个性质
性质 含义 底层靠什么保证
互斥 同一刻只有一人持锁 原子 CAS / set nx
可重入 同一线程可重复加锁 记录 holder + 计数
防死锁 持有者崩溃也能自动释放 租约 TTL(lease) 自动过期
高可用 锁服务挂了仍能工作 多副本 + 多数派
公平 先来先得,不饿死 顺序节点 / 队列
最容易被忽视的是"防死锁"与"锁失效":TTL 太短 → 业务没干完锁就过期,别人进来 → 两个人都以为自己持锁(锁失效 );TTL 太长 → 持有者崩溃后要等很久才释放。这正是"看门狗续期"和"fencing token"要解决的。
4.2 Redis:set nx ex + 看门狗 + Redlock
基础版(单实例):
SET lock_key unique_value NX EX 30 # 不存在才设(NX),30秒过期(EX)
# 释放时务必用 Lua 脚本校验 value 再删,防误删别人的锁
if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) end
unique_value: 每个客户端用唯一值(如 UUID+线程号),释放时校验"这是我的锁才删",避免 A 的锁过期后 B 拿到、A 回来误删 B 的锁。
看门狗(Watchdog)续期: Redisson 等客户端在持锁期间起一个后台定时任务,每隔 TTL/3 就重置过期时间 ,只要客户端还活着,锁就不过期;客户端崩溃 → 定时任务没了 → 锁自然过期释放。完美解决"TTL 设多长"的两难。
Redlock(多实例,Antirez 提出): 为去掉"单 Redis 挂了锁就没了"的单点风险。向 N=5 个独立的 Redis master 依次加锁:
记录起止时间,向 5 个节点都尝试 SET NX EX(各带独立短超时);
若拿到 ≥ N/2+1 = 3 个 锁,且总耗时 < 锁有效期 → 认为加锁成功;
失败/用完则向所有节点释放。
Client
记 T1
Redis1
Redis2
Redis3
Redis4
R5
成功条件:拿到 ≥ 3 把 且 总耗时 < TTL
① 顺序向 5 个独立 master 加锁(各自短超时,不阻塞等)。
② 记 T2,elapsed = T2-T1。
③ elapsed < TTL 且 成功数 ≥ 3 → 有效锁。
④ 否则向所有节点释放,稍后重试。
图 5:Redlock 流程。向 5 个独立 Redis 加锁,多数派(≥3)且未超 TTL 才算成功;用"多节点异构"降低单点风险。
Martin Kleppmann 的著名质疑: Redlock 依赖"各节点本地时钟"判断 TTL 过期——若某节点发生 GC 停顿 / 时钟跳变,可能在一个客户端"还以为持锁"时锁已被判过期,造成两人同时持锁。他认为 Redlock 无法在异步模型下保证正确性,应改用"带 fencing token 的存储"(见 §4.4)。Antirez 反驳称合理假设下仍可用。结论:Redlock 提升了可用性,但严格正确性存争议;追求强正确请用 fencing。
4.3 ZooKeeper / Etcd / 数据库 实现
ZooKeeper(临时顺序节点): 在 /lock 下创建临时顺序节点 (如 lock-00000001)。判断"自己是不是序号最小的那一个":是 → 持锁;否 → 只 watch 比自己小一号的节点 。前驱释放(节点删)才通知自己重试。羊群效应改进: 只 watch 前驱而非全体,避免"一个释放、所有人被唤醒"的惊群。
Etcd(lease + 原子 CAS + watch): 用事务 put key value with lease 且 prevKey 为空(CAS 抢锁);持锁靠 lease 自动过期防死锁;watch 前驱 key 实现公平等待。比 ZK 更轻、watch 还能回放。
数据库(唯一索引 / 乐观锁):
唯一索引: INSERT 一条 (resource, owner) 记录,插入成功即获锁;释放即删。靠 DB 唯一约束保证互斥(最简单但 DB 压力大)。
乐观锁 version: 更新时带 where version = 旧值,版本不匹配则失败——适合"并发更新同一条数据"而非严格互斥锁场景。
取舍 ZK/Etcd 锁靠"共识 + 租约 + watch",正确性天然好、公平、防死锁;代价是多一次网络往返 + 依赖协调服务。Redis 锁最轻量高性能,但"防失效"要自己用看门狗/fencing 补。
4.4 fencing token:锁失效的终极补丁
即使有 TTL,仍可能因 GC 停顿 / 网络延迟,出现"A 持锁期间锁过期,B 拿到锁,然后 A 苏醒继续写 "——两人都以为自己合法。fencing token 是根治这类的方案。
原理: 每次成功加锁,锁服务发一个单调递增的整数 token 。客户端去操作"受保护的资源(如存储)"时,必须带上 token ;资源方(存储)记住"已处理的最大 token",只接受 token > 已见最大值的写 ,旧 token 的写直接拒绝。
Client A
token=1
Client B
token=2
Storage
记 max=0
A 加锁成功 → token=1
B 加锁成功 → token=2
A 持锁期间 GC 停顿 / 网络延迟
→ 锁 TTL 过期,B 趁机拿到锁
A 苏醒,带 token=1 写 → 被拒(1 < max=2)
B 带 token=2 写 → 接受,max 更新为 2
图 6:fencing token 防护。A 的 token=1 过期后 B 拿 token=2;A 苏醒带旧 token 写,存储因"1 < 已知最大 2"而拒绝,杜绝两人同时改。
一句话记住 fencing token 把"我是否还合法"的判断题,从"锁服务说了算"变成"每次写都带单调递增令牌、存储只认最新的那个"——即使锁失效,旧客户端的写也会被挡在门外。
4.5 横向对比表
方案 互斥原语 防死锁 正确性 性能 公平 适用
Redis(set nx ex) 原子 SET NX TTL + 看门狗 中(需 fencing 补) 高 否(非公平) 高并发、可容忍极小概率失效
Redlock 多节点 NX TTL 中(时钟依赖争议) 中高 否 要去掉单点、仍非强正确
ZooKeeper 临时顺序节点+CAS 会话/租约 高 中 是 强正确、公平、可靠
Etcd lease + CAS + watch lease 高 中高 是 云原生、强正确
数据库 唯一索引/version 需自己管 高(单库) 低 否 简单场景、不想引入中间件
取舍 要"最快最轻"→ Redis(记得加看门狗,关键场景加 fencing);要"绝对正确 + 公平"→ ZK/Etcd;要"零中间件"→ DB 唯一索引(扛不住高并发)。
5. 一张图回到底层路线
把三个组件叠回"共识 + 租约"的内核,你会发现它们高度同源:
组件 共识(CP/AP) 租约 lease watch 推送 互斥原语
注册中心 目录一致性(Raft/ZAB/Distro) 实例存活 TTL 列表变更推送 —
ID 生成 workerId 分配(共识/号段) 无(本地时钟) 无 内存/原子发号
分布式锁 抢锁顺序一致性 锁 TTL 防死锁 前驱释放通知 CAS/顺序节点
一句话记住 三个组件 = 同一内核(共识定序 + 租约授权 + watch 通知 + 互斥抢权)在"目录 / 编号 / 互斥"三个面的投影。学框架前先吃透这四样,框架只是它们的工程化外壳。
6. 一句话路线总结
底层路线(务必背住这一条):
分布式协调的本质 = 共识(Raft/ZAB,多数派定序) + 租约(lease,带期限授权、靠本地倒计时绕开时钟难题) + watch(状态变更推送) + 互斥原语(CAS / 顺序节点) 。
注册中心把这四样拼成"服务目录";ID 生成把"共识分配 workerId + 本地时钟/序列"拼成"趋势有序编号";分布式锁把"原子抢权 + 租约防死锁 + fencing 防失效"拼成"互斥"。框架年年换,这套机制不换——底层通,则框架只是实例化与取舍。
给跨专业同学的学习建议: 遇到任何"分布式协调"类框架,先问四个问题——①它用什么保证一致(共识还是最终一致)?②存活/过期靠租约还是心跳?③变更怎么通知(watch 还是轮询)?④抢资源靠什么互斥(CAS / 顺序节点 / 唯一索引)?答完这四点,框架的"黑盒"就透明了。