分布式协调原语:注册中心、ID生成、分布式锁

一致性的底层 + 三个最常用协调组件的技术对比

本主题的底层路线(万变不离其宗):
注册中心 / ID 生成 / 分布式锁 的底层 = 一致性协议(Raft / ZAB) + 心跳 / 租约(lease) + 订阅回调(watch) + 互斥原语
它们不是三种技术,而是「共识 + 租约」这套机制的三种应用:注册中心用「共识保证目录一致 + 租约判定存活 + watch 推送变更」;ID 生成用「共识分配 workerId + 本地时钟/序列保证趋势有序」;分布式锁用「共识/原子操作抢互斥权 + 租约防死锁 + fencing 防失效」。一句话记住 先把"共识 + 租约"想透,三个组件只是它长出的三张脸。
目录 0. 读前必读:三个组件,其实是一套机制
1. 一致性基础:CAP / BASE / quorum / lease / watch / 共识
 1.1 CAP 与"三选二"的真相
 1.2 BASE 与最终一致
 1.3 quorum:N/2+1 多数派
 1.4 lease 租约:带期限的授权
 1.5 watch / 回调订阅
 1.6 共识简述:Raft / ZAB / Paxos
2. 注册中心:把"共识 + 租约"用成了服务目录
 2.1 注册中心到底在协调什么
 2.2 ZooKeeper(ZAB + 临时节点 + watch)
 2.3 Etcd(Raft + lease + watch)
 2.4 Nacos(AP Distro + CP Raft 双模型)
 2.5 Eureka(AP + 心跳 + 自我保护)
 2.6 Consul(Raft + gossip + 多数据中心)
 2.7 横向对比表与选型
3. 序列号 / ID 生成器:给每件事一个全局有序的"身份证"
 3.1 UUID 与数据库自增
 3.2 号段模式(Leaf-segment)
 3.3 Snowflake 与时钟回拨
 3.4 Redis incr 与 Leaf-snowflake / UidGenerator
 3.5 横向对比
4. 分布式锁:把"互斥"这件事做对
 4.1 锁必须满足的五个性质
 4.2 Redis:set nx ex + 看门狗 + Redlock
 4.3 ZooKeeper / Etcd / 数据库 实现
 4.4 fencing token:锁失效的终极补丁
 4.5 横向对比表
5. 一张图回到底层路线
6. 一句话路线总结

0. 读前必读:三个组件,其实是一套机制

你可能在各种框架文档里被"注册中心 / 雪花算法 / 分布式锁"这三个名字搞晕——它们看起来毫不相关。但底层它们共享同一套协调基础设施

本部分的认知顺序:先打牢"一致性底层"(CAP、quorum、lease、watch、共识算法)→ 再看三个组件如何各自把这套机制实例化并做出取舍。读每一节都问自己一句:"它用的是共识、还是租约、还是互斥?在哪个点上做了取舍?"
一句话记住 注册中心、ID、锁不是三件事,而是"共识 + 租约"这一个内核被用在了"服务目录、时间编号、互斥权"三个场景里。

1. 一致性基础:CAP / BASE / quorum / lease / watch / 共识

所有协调组件都建立在对"一致性与可用性如何取舍"的理解上。这一节是地基,后面三节都会回头引用它。

1.1 CAP 与"三选二"的真相

CAP 定理:一个分布式数据系统在网络分区(P,Partition)发生时,一致性(C,Consistency)可用性(A,Availability)无法同时保证,只能二选一。

真相:P 通常"不得不保"。 只要你的系统是分布式的、跨节点的,网络分区就不可避免(网线断了、交换机抽风、GC 停顿假死都是"分区"的表现)。所以现实里不是"三选二",而是:保 P,然后在 C 和 A 之间取舍
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 相对):

直觉:AP 注册中心(如 Eureka)就是 BASE 的化身——节点下线后,别的节点可能短暂拿到旧列表(软状态),但心跳恢复 / 重新拉取后最终会一致。消息队列、缓存、DNS 也都是最终一致。取舍 用"短暂不一致"换"永远不掉线"。

1.3 quorum:N/2+1 多数派

在"多副本"系统里,怎么判断一个值"算数"?靠法定人数(quorum)。设副本数 N,写法定数 W、读法定数 R。为保证读到的一定是"最新写过的",经典约束是:

W + R > N

最常用的是 W = R = N/2 + 1(多数派)。意思是:写要被超过半数的节点确认,读也要从超过半数的节点读取并取最新——两者必然交集,所以读到的一定是含最新写的那一份。

一句话记住 quorum 的核心是"W+R>N"——让"写集合"和"读集合"必然相交,从而读到最新;多数派 N/2+1 是最常用的那一档。

1.4 lease 租约:带期限的授权

很多协调问题可归结为"谁有权做某事"(谁是主、谁能拿锁、谁还活着)。最优雅的机制之一是租约(lease)

租约 = 一方给另一方一张"在 T 时间内有效的授权"。持有者在 T 内可放心行使权利;T 到期前必须续约(renew),否则授权自动失效。

为什么需要 lease?缓解"时钟漂移"。 分布式里没有全局一致的时钟,你不能说"10:00:00 整所有人同时失效"。lease 把"失效判定"交给定时器(本地倒计时),而不是靠节点间对表。只要租约长度 ≫ 最大时钟误差,就不会出现"两个人都以为自己合法"的危险窗口。
一句话记住 lease 是"带期限、需续约的授权";它用本地倒计时绕开了"分布式没有统一时钟"的难题,是防死锁、判存活的万能钥匙。

1.5 watch / 回调订阅

协调系统的第二大能力是"状态变了通知我",而不是让我不停去问(轮询)。这叫 watch / 订阅-推送

一句话记住 watch 让"配置/服务列表变化"从"客户端轮询"变成"服务端推送";但务必确认它是"一次性"还是"可回放",否则会漏事件。

1.6 共识简述:Raft / ZAB / Paxos

"多个节点对某个值的顺序达成一致"叫共识(consensus)。它是 CP 系统的引擎。

Raft(Etcd / Consul / Nacos-CP 用)

Raft 把共识拆成三个易理解的子问题:

  1. 领导选举(Leader Election):节点有 Follower / Candidate / Leader 三种角色。Follower 收不到 Leader 心跳超时 → 变 Candidate,投自己一票并要别人投票;拿到多数派票 → 当 Leader。每届叫一个 Term(任期),Term 单调递增,防止旧 Leader 乱发号。
  2. 日志复制(Log Replication):所有写先到 Leader;Leader 把命令作为"日志条目"追加,并复制到 Follower;收到多数派 ack 后,Leader 把它标记为已提交(committed),再应用到状态机,并通知 Follower 提交。
  3. 安全性(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)

取舍 ZK 强一致、可靠、功能全,但写受限单主、运维偏重(要奇数节点、要小心 znode 数量),不适合超高频写或海量 key。

2.3 Etcd(Raft + lease + watch)

一句话记住 Etcd = "Raft 保证一致 + revision 做版本 + lease 判存活 + watch 可回放";K8s 的选择,云原生第一公民。

2.4 Nacos(AP Distro + CP Raft 双模型)

Nacos 的聪明之处在于"一套产品,两种一致性按需切换"

取舍 Nacos 用"注册 AP、配置 CP"的拆分,兼顾了"服务发现要永远在线"和"配置要绝对一致"两套诉求。代价是内部两套复制机制、理解成本略高。

2.5 Eureka(AP + 心跳 + 自我保护)

为什么 Eureka 敢 AP? 服务发现"偶尔拿到一个已下线实例"的后果,通常只是一次重试;而"注册中心在分区时拒绝所有发现请求"的后果是整个系统瘫痪。所以 Netflix 选择可用性优先。这也是 CAP 取舍的教科书案例。

2.6 Consul(Raft + gossip + 多数据中心)

2.7 横向对比表与选型

组件一致性底层协议健康判定推送方式性能/特点典型场景
ZooKeeperCPZAB临时节点(会话/租约)watch(一次性)可靠但写受限单主;运维重强一致注册/锁/选主/配置
EtcdCPRaftlease TTLwatch(可回放)云原生、gRPC、revision 强K8s 生态、配置、锁
NacosAP+CP 双Distro / Raft心跳 + 探活推送 + 拉取注册配置合一、易运维Spring Cloud 微服务体系
EurekaAP异步复制心跳 + 自我保护客户端拉取+缓存高可用、容忍短暂不一致纯服务发现、容错优先
ConsulCPRaft + gossip丰富健康检查watch / 阻塞查询原生多数据中心、多合一多 DC、基础设施一体
一句话记住 选注册中心先问"你能接受短暂不一致吗":能 → Eureka/Nacos(AP);不能、且要强一致 → ZK/Etcd/Consul(CP);想注册配置一起管 → Nacos;要跨数据中心 → Consul。

3. 序列号 / ID 生成器:给每件事一个全局有序的"身份证"

分布式下,多机各自自增会撞号。ID 生成器的核心诉求:全局唯一尽量趋势递增(让数据库索引更友好)、高性能高可用。下面按"底层机制"从弱到强对比。

3.1 UUID 与数据库自增

UUID 当主键是"索引杀手":InnoDB 主键即聚簇索引,无序插入导致页分裂 + 内存碎片,写入放大明显。

3.2 号段模式(Leaf-segment)

思路:数据库只负责"批发号段",应用内存"零售发号"

  1. DB 里存 max_id, step;应用去取号段 [max_id+1, max_id+step],并把 max_id 更新为 max_id+step。
  2. 应用拿到号段后在本地内存原子自增发号,完全不碰 DB,性能极高。
  3. 双 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 位序列号在同一毫秒内自增。
时钟回拨怎么处理? ① 小回拨(如<5ms):等时间追平再发;② 大回拨:抛异常或切换备用 workerId / 扩展位借用;③ 根本上——用 Leaf-snowflake(见下)把 workerId 上收托管,并缓存近期时间戳做检测。

3.4 Redis incr 与 Leaf-snowflake / UidGenerator

一句话记住 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

Redlock(多实例,Antirez 提出):为去掉"单 Redis 挂了锁就没了"的单点风险。向 N=5 个独立的 Redis master 依次加锁:

  1. 记录起止时间,向 5 个节点都尝试 SET NX EX(各带独立短超时);
  2. 拿到 ≥ N/2+1 = 3 个锁,且总耗时 < 锁有效期 → 认为加锁成功;
  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 / 数据库 实现

取舍 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 NXTTL + 看门狗中(需 fencing 补)否(非公平)高并发、可容忍极小概率失效
Redlock多节点 NXTTL中(时钟依赖争议)中高要去掉单点、仍非强正确
ZooKeeper临时顺序节点+CAS会话/租约强正确、公平、可靠
Etcdlease + CAS + watchlease中高云原生、强正确
数据库唯一索引/version需自己管高(单库)简单场景、不想引入中间件
取舍 要"最快最轻"→ Redis(记得加看门狗,关键场景加 fencing);要"绝对正确 + 公平"→ ZK/Etcd;要"零中间件"→ DB 唯一索引(扛不住高并发)。

5. 一张图回到底层路线

把三个组件叠回"共识 + 租约"的内核,你会发现它们高度同源:

组件共识(CP/AP)租约 leasewatch 推送互斥原语
注册中心目录一致性(Raft/ZAB/Distro)实例存活 TTL列表变更推送
ID 生成workerId 分配(共识/号段)无(本地时钟)内存/原子发号
分布式锁抢锁顺序一致性锁 TTL 防死锁前驱释放通知CAS/顺序节点
一句话记住 三个组件 = 同一内核(共识定序 + 租约授权 + watch 通知 + 互斥抢权)在"目录 / 编号 / 互斥"三个面的投影。学框架前先吃透这四样,框架只是它们的工程化外壳。

6. 一句话路线总结

底层路线(务必背住这一条):
分布式协调的本质 = 共识(Raft/ZAB,多数派定序) + 租约(lease,带期限授权、靠本地倒计时绕开时钟难题) + watch(状态变更推送) + 互斥原语(CAS / 顺序节点)
注册中心把这四样拼成"服务目录";ID 生成把"共识分配 workerId + 本地时钟/序列"拼成"趋势有序编号";分布式锁把"原子抢权 + 租约防死锁 + fencing 防失效"拼成"互斥"。框架年年换,这套机制不换——底层通,则框架只是实例化与取舍。
给跨专业同学的学习建议:遇到任何"分布式协调"类框架,先问四个问题——①它用什么保证一致(共识还是最终一致)?②存活/过期靠租约还是心跳?③变更怎么通知(watch 还是轮询)?④抢资源靠什么互斥(CAS / 顺序节点 / 唯一索引)?答完这四点,框架的"黑盒"就透明了。
Part 04 · 分布式协调原语:注册中心、ID生成、分布式锁
配套 Markdown 版本见同名 .md 文件。图示建议结合 HTML 版查看。