← 返回场景决策

🐘 ZooKeeper 分布式协调实战

结果导向 · 分布式锁 / 注册中心 / 选举 / 配置中心 / Watch 监听

ZooKeeper 分布式锁 注册中心 Leader选举 Watch监听 ZAB协议 临时节点

① 直接给结论

ZooKeeper 是什么(一句话):一个分布式协调服务——它自己不存业务数据、不跑业务逻辑,专门帮分布式系统做协调工作:选主、加锁、注册发现、配置同步。像分布式系统的"村委会",不干活,专门通知大家、维持秩序。

什么场景该用 ZooKeeper:

场景ZK 在里面干什么不用会怎样
分布式锁临时节点 + Watch 实现公平锁,会话断开自动释放自己用 Redis 实现也行,但 ZK 更强一致、不会脑裂
注册中心(Dubbo/Kafka)服务上下线通知,客户端 Watch 节点变化Dubbo 老版本默认用 ZK,Kafka 元数据也存 ZK
Leader 选举多实例竞争创建同一个临时节点,创建成功的当 Leader自己实现选举逻辑很复杂,容易出 bug
配置中心配置存在 ZNode,客户端 Watch 变化自动刷新现在更推荐 Nacos/Apollo,ZK 做配置太重
分布式 ID(不推荐)顺序节点生成唯一序号有更好的方案(雪花算法),ZK 性能不够
⚠️ 重要认知:ZK 不适合存业务数据。它是个协调工具,不是数据库。单机写入 QPS 上限约 5000-10000,ZNode 数据上限 1MB。把用户数据、订单数据往 ZK 塞是严重误用。ZK 存的是"元数据"和"协调状态"——谁是 Leader、谁在线、锁被谁持有。
一句话选型:新项目注册中心/配置中心 → 选 Nacos,不要选 ZK(ZK 做这些太重、运维麻烦)。需要强一致分布式锁或 Leader 选举 → ZK 是经典选择,但 Redis(Redisson)也能做且更轻。ZK 现在主要用于Kafka 元数据存储Dubbo 老项目,新项目用得越来越少。

② ZooKeeper 是什么 + 能力全景

定位:ZooKeeper 是一个树形结构的内存数据库 + 事件通知系统。它的核心是 ZNode 树(类似文件系统目录),每个节点可以存少量数据,客户端可以"监听"节点变化,节点一变就收到通知。基于这两个能力,衍生出分布式锁、注册中心、选举等上层应用。

关键认知:ZK 的本质是"公告栏 + 订阅通知"
公告栏:ZNode 树就是公告栏,谁上线了写一条、谁下线了擦掉。
订阅通知:Watch 机制就是订阅,公告栏一变,订阅者立刻收到通知。
分布式系统里所有"协调"问题,本质都是"大家怎么知道彼此状态"——ZK 就是那个让大家都知道彼此状态的中间人。
ZooKeeper 工作模型
ZK Server 集群
(Leader + Follower)
客户端 A
(创建节点/Watch)
客户端 B
(Watch 节点)
客户端 C
(Watch 节点)
A 创建/删除节点 → ZK 通知所有 Watch 该节点的客户端(B、C)

核心能力全景(5 大块):

📂
ZNode 树存储
树形节点结构,类似文件系统。每个节点可存 1MB 数据。持久节点/临时节点/顺序节点三种类型。
🔔
Watch 事件通知
客户端监听节点变化,节点创建/删除/数据变更时收到通知。一次性触发,需重新注册。
👑
Leader 选举
基于临时节点:多实例竞争创建同一节点,成功的当 Leader。Leader 挂了自动重选。
🔐
分布式锁
临时顺序节点 + Watch 前一个节点,实现公平锁。会话断开锁自动释放,防死锁。
📋
注册中心
服务启动创建临时节点,消费者 Watch。服务挂了节点消失,消费者收到下线通知。
⚙️
配置中心
配置存 ZNode,客户端 Watch 变化。改配置即推送,但功能弱,现在用 Nacos 更多。
PRO TIP ZK 最核心的两个概念:临时节点 + Watch。临时节点的生命周期绑定会话——客户端断开,节点自动删除,这就是"服务下线自动感知"和"锁自动释放"的底层原理。Watch 是一次性通知——节点变了通知你一次,要看继续变化得重新注册,这个设计既是优点(不会通知风暴)也是坑(容易漏事件)。

③ ZooKeeper vs 其他协调/注册方案

常被拿来和 ZK 比的:Nacos(阿里注册中心+配置)、Eureka(Netflix 注册中心)、etcd(K8s 用的)、Consul(HashiCorp)。结果导向直接看下表:

维度ZooKeeperNacosEurekaetcdConsul
定位分布式协调(通用)注册中心+配置中心注册中心KV存储+协调注册+配置+健康检查
一致性CP (ZAB,强一致)AP/CP 可切换AP (最终一致)CP (Raft)CP (Raft)
语言JavaJavaJavaGoGo
注册中心⚠️ 能做但重✅ 专精,功能全✅ 简单⚠️ 偏底层✅ 功能全
配置中心⚠️ 能做但弱✅ 强(灰度/版本)❌ 不支持⚠️ 偏底层✅ 支持
分布式锁✅ 经典方案⚠️ 不推荐✅ 支持✅ 支持
运维难度⚠️ 高(Java,GC)✅ 低(Docker友好)✅ 低✅ 低(单二进制)✅ 低
什么时候选它Kafka元数据/Dubbo老项目/强一致锁✅ Spring Cloud新项目首选维护中(2.x停更)K8s生态多数据中心
实际怎么选:Spring Cloud 新项目注册中心+配置中心 → Nacos(90% 场景)。Kafka 集群 → ZK(Kafka 自己用,你没得选)。K8s 生态 → etcd(K8s 内置)。需要强一致分布式锁 → ZK 或 Redis(Redisson)。ZK 在新项目里用得越来越少,主要存量在 Kafka 和 Dubbo 老项目里。

④ 核心原理(ZNode / Watch / 选举 / ZAB)

4.1 ZNode 树结构(ZK 的数据模型)

ZK 的数据是树形结构,和文件系统目录一样。每个节点叫 ZNode,可以存数据、可以有子节点。根节点是 /

🐘 ZK 节点树 树结构 节点详情 / (root)
/ # 根节点 ├── /app1 # 持久节点(应用1的根) │ ├── /app1/config # 持久节点(存配置) │ │ └── data: {"timeout":30, "retry":3} # 节点数据(最多1MB) │ ├── /app1/leader # 临时节点(谁是Leader) │ │ └── data: "server-01" (ephemeral) # 创建者活着才存在 │ └── /app1/members # 持久节点(成员目录) │ ├── /app1/members/server-01 # 临时节点(server-01在线) │ ├── /app1/members/server-02 # 临时节点(server-02在线) │ └── /app1/members/server-03 # 临时节点(server-03在线) │ └── data: "192.168.1.3:8080" ├── /app2 │ └── /app2/locks │ ├── /app2/locks/lock-00000001 # 顺序临时节点(锁持有者) │ ├── /app2/locks/lock-00000002 # 顺序临时节点(等待者1) │ └── /app2/locks/lock-00000003 # 顺序临时节点(等待者2) └── /zookeeper # ZK内置节点(别动) └── /zookeeper/quota
🟦 持久节点   🟨 临时节点(ephemeral,会话断开自动删)   🟩 顺序节点(自动加序号)
节点类型特性典型用途
持久节点 (PERSISTENT)创建后一直存在,主动删除才消失配置、目录结构
持久顺序节点持久 + 自动加递增序号后缀分布式队列
临时节点 (EPHEMERAL)会话断开自动删除,不能有子节点注册中心、Leader选举、分布式锁
临时顺序节点临时 + 顺序序号分布式公平锁(最常用)
临时节点是 ZK 的灵魂。它和客户端会话绑定——客户端活着它就在,客户端断开(崩溃/网络故障)它自动消失。这个特性让 ZK 能做到:服务挂了自动从注册中心摘掉、持锁者挂了锁自动释放。不需要任何人工干预或心跳检测代码,ZK 帮你做了。

4.2 Watch 机制(一次性通知)

Watch 是 ZK 的事件订阅机制。客户端读节点时可以注册 Watch,节点变化时 ZK 推送通知。

客户端B
getData("/app1/config", watch=true)
ZK
记录:B监听/app1/config
客户端A
setData("/app1/config", newData)
ZK 客户端B
收到通知:NodeDataChanged
⚠️ 通知是一次性的:B 收到通知后要重新注册 Watch 才能监听下一次变化
Watch 的"一次性"是双刃剑:优点——不会通知风暴,每个事件精确通知一次。缺点——重新注册 Watch 有时间窗口,在"收到通知"和"重新注册"之间发生的变更会漏掉。这是 ZK 最常见的坑,后面坑与事故会专门讲。ZK 3.6+ 增加了持久 Watch(persistentWatch)解决这个问题。

4.3 Leader 选举(ZAB 协议)

ZK 集群一般是奇数台(3/5/7),用 ZAB 协议保证强一致。ZAB 协议做两件事:①选 Leader ②数据同步。

Leader 选举过程(3 台集群为例)
初始:3台都是 LOOKING 各自投票给自己 互相交换选票
比较(myid, zxid) zxid大的优先
(数据更新的)
zxid相同比myid
server-2 票数过半 server-2 → LEADING server-1/3 → FOLLOWING
PRO TIP 为什么 ZK 集群要奇数台?ZK 用"过半写成功"保证一致性——3 台挂 1 台还能用(2>1.5),4 台也是挂 1 台能用(3>2),但 4 台挂 2 台就废了(2=2 不过半)。3 台和 4 台的容错能力一样(都只能挂 1 台),但 4 台多花一台机器钱。所以奇数台性价比最高:3 台容错 1 台,5 台容错 2 台,7 台容错 3 台。

⑤ 核心配置实战

5.1 Spring Boot 引入 Curator(ZK 客户端)

pom.xml
<!-- Curator 是 ZK 的高级客户端,比原生 API 好用 --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>5.5.0</version> </dependency> <!-- Spring Boot 集成 --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-x-spring</artifactId> <version>5.5.0</version> </dependency>

5.2 分布式锁(InterProcessMutex,最常用)

DistributedLock.java
📁 java/
DistributedLock.java
⭐ 当前
LeaderElection.java
ConfigWatcher.java
📁 resources/
application.yml
@Component public class DistributedLock { private final CuratorFramework client; private static final String LOCK_PATH = "/locks/order-create"; @Autowired public DistributedLock(CuratorFramework client) { this.client = client; } public <T> T executeWithLock(Supplier<T> task) throws Exception { // InterProcessMutex: 可重入公平锁(基于临时顺序节点) InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH); try { // 尝试加锁,最多等 5 秒 if (!lock.acquire(5, TimeUnit.SECONDS)) { throw new RuntimeException("获取分布式锁超时"); } // 拿到锁,执行业务 return task.get(); } finally { // 释放锁(必须放finally) if (lock.isAcquiredInThisProcess()) { lock.release(); } } } } // 使用 @Autowired DistributedLock distributedLock; public Order createOrder(OrderRequest req) throws Exception { return distributedLock.executeWithLock(() -> { // 这里同一时刻只有一个线程能进来(跨JVM) return doCreateOrder(req); }); }
InterProcessMutex 的原理:①在 /locks/order-create 下创建临时顺序节点 lock-00000001;②获取所有子节点,自己是不是最小的?是最小→拿到锁;不是→Watch 前一个节点;③前一个节点删除(释放锁或崩溃),自己变最小→拿到锁;④释放锁=删除自己的节点。这就是 ZK 分布式锁的"公平锁"实现,每个等待者只 Watch 前一个,避免羊群效应。

5.3 Leader 选举(LeaderLatch)

LeaderElection.java
@Component public class LeaderElection implements LeaderLatchListener { private LeaderLatch leaderLatch; @PostConstruct public void init() throws Exception { // LeaderLatch: 多实例竞争,始终一个是Leader leaderLatch = new LeaderLatch(client, "/election/job-scheduler", serverId); leaderLatch.addListener(this); leaderLatch.start(); } @Override public void isLeader() { // 当选Leader - 开始执行定时任务 log.info("我当选Leader了,开始调度任务"); scheduler.start(); } @Override public void notLeader() { // 失去Leader - 停止定时任务 log.info("失去Leader身份,停止调度"); scheduler.stop(); } }
PRO TIP Leader 选举最典型的场景:定时任务去重。3 台机器都部署了定时任务,但任务只想跑一次。用 Leader 选举——3 台竞争,只有 Leader 那台跑定时任务,其他两台待命。Leader 挂了,剩下的自动重选新的 Leader 接替。比分布式锁更优雅——锁是"临时抢一下",选举是"长期持有直到挂"。

⑥ 生产场景(3 个带界面演示)

场景 1:分布式锁防超卖(秒杀场景)

背景:秒杀活动,3 台应用实例同时收到扣库存请求。MySQL 的 UPDATE stock SET num=num-1 在高并发下会超卖。用 ZK 分布式锁串行化扣库存操作。

🔐 ZK 分布式锁面板 /locks/stock-10086 实时 ▾
锁持有者
server-02
持有 1.2s
等待队列
4
4个等待
今日加锁次数
12,483
秒杀开始后
超卖
0
✓ 零超卖
/locks/stock-10086 下的临时顺序节点:
持有中 /locks/stock-10086/lock-00000128 server-02
等待 #1 /locks/stock-10086/lock-00000129 server-01
等待 #2 /locks/stock-10086/lock-00000130 server-03
等待 #3 /locks/stock-10086/lock-00000131 server-02
等待 #4 /locks/stock-10086/lock-00000132 server-01
已释放 /locks/stock-10086/lock-00000127 server-03 (已删)
⚠️ 每个等待者只 Watch 前一个节点(避免羊群效应):#129 监听 #128, #130 监听 #129...
ZK 分布式锁 vs Redis 分布式锁:ZK 锁强一致(CP),不会脑裂;Redis 锁性能好(10 万 QPS),但主从切换时可能脑裂。秒杀场景宁可慢不能错,选 ZK 锁。普通场景 Redis 锁够用。

场景 2:定时任务 Leader 选举(防重复执行)

背景:报表生成任务部署在 3 台机器上,但任务每天只该跑一次。用 Leader 选举——3 台竞争,只有 Leader 执行,Leader 挂了自动切换。

👑 Leader 选举面板 /election/report-job 凌晨2点 ▾
当前 Leader
server-01
任期 3天
候选者
3
server-01/02/03
今日执行
1
✓ 只跑一次
切换次数
2
本月故障切换
选举状态时间线:
[06-16 02:00:00] server-01 当选 Leader,开始执行报表任务
[06-17 14:23:00] server-01 宕机! 检测到会话超时,临时节点删除
[06-17 14:23:03] server-02 当选新 Leader(3秒切换)
[06-18 02:00:00] server-02 执行报表任务 ✓
[06-18 10:00:00] server-01 恢复,作为 Follower 待命
[06-19 02:00:00] server-02 继续执行报表任务(Leader未变)
PRO TIP Leader 选举 vs 分布式锁做定时任务去重:锁是"每次任务执行前抢一下",选举是"长期持有 Leader 身份"。定时任务用选举更好——不用每次抢锁,Leader 直接跑,省一次 ZK 往返。而且选举切换是秒级的(会话超时检测),比锁的"等待+重试"更稳定。

场景 3:服务注册发现(Dubbo 场景)

背景:Dubbo 服务提供者启动时注册到 ZK,消费者从 ZK 拉取地址列表。提供者挂了,ZK 自动通知消费者摘除节点。

📋 服务注册面板 /dubbo/com.order.Service/providers 实时 ▾
在线提供者
5
5个实例
消费者
12
12个实例
今日下线
2
发版+1,故障+1
通知延迟
<1s
Watch秒级推送
/dubbo/com.order.Service/providers 下的临时节点:
在线 dubbo://192.168.1.10:20880/com.order.Service version=1.0
在线 dubbo://192.168.1.11:20880/com.order.Service version=1.0
在线 dubbo://192.168.1.12:20880/com.order.Service version=2.0
在线 dubbo://192.168.1.13:20880/com.order.Service version=2.0
在线 dubbo://192.168.1.14:20880/com.order.Service version=2.0
已下线 dubbo://192.168.1.15:20880/com.order.Service 10秒前宕机,自动摘除
⚠️ 提供者宕机 → 会话超时 → 临时节点自动删除 → Watch 的消费者收到通知 → 摘除地址(全程自动,无需人工)
注册中心的"临时节点"是关键:服务提供者启动时创建临时节点(地址写在节点数据里),只要它活着,节点就在;一旦崩溃或网络断开,会话超时后节点自动消失,消费者通过 Watch 收到下线通知。不需要任何心跳代码——ZK 的会话机制天然就是心跳。这也是为什么 Dubbo 老版本选 ZK 做注册中心。

⑦ 坑与事故(4 个真实翻车)

事故 1:Watch 一次性触发导致漏事件,配置没刷新

现象:配置中心用 ZK 实现,改了配置,部分实例刷新了、部分没刷新。排查发现 Watch 通知漏了。

原因:Watch 是一次性的。流程是:①收到通知 ②重新 getData 注册 Watch ③读取新数据。但①和②之间有时间窗口,如果这期间配置又改了,第二次变更不会被通知,因为 Watch 还没重新注册。

解法:①先重新注册 Watch 再处理数据(顺序反过来);②用 ZK 3.6+ 的 addPersistentWatch()(持久 Watch,不用重新注册);③新项目直接用 Nacos 做配置中心,别用 ZK。

事故 2:Session 超时假死,服务不上下线

现象:某服务 GC 停顿 40 秒,ZK 会话超时(默认 40 秒),临时节点被删,服务被摘除。但服务其实还活着,GC 结束后重新连上 ZK 又注册回来。在这期间消费者认为它下线了,流量全打到其他实例,差点雪崩。

原因:ZK 会话超时是硬性的——不管你是真挂了还是 GC 假死,超时就删临时节点。JVM Full GC 导致 STW,客户端没法发心跳,ZK 认为你挂了。

解法:①调大 sessionTimeout(从 40s 调到 120s,容忍 GC);②优化 JVM 避免 Full GC;③用 Nacos——Nacos 是客户端主动心跳,GC 假死时心跳会恢复,不会误判下线(AP 模式更宽容)。

事故 3:羊群效应,万级客户端同时收到通知

现象:某个服务有 1 万个消费者 Watch 了同一个节点。该节点一变,1 万个通知同时发出,ZK CPU 飙到 100%,网络瞬间打满。

原因:这是 ZK 经典的"羊群效应"——太多客户端 Watch 同一个节点,节点变化时全部通知。

解法:分布式锁用临时顺序节点 + 只 Watch 前一个(InterProcessMutex 就是这么做的),避免羊群效应。注册中心场景改用子节点列表 Watch(getChildren + watch)而不是单个节点 Watch,减少通知范围。

事故 4:ZK 集群脑裂,写丢失

现象:5 台 ZK 集群,网络分区导致 2 台一组、3 台一组。3 台那组选举出 Leader 继续服务,2 台那组因不过半无法选举。分区恢复后,2 台那组的数据被丢弃(因为 3 台那组 zxid 更新)。

原因:ZK 的 ZAB 协议要求过半写成功,网络分区时少数派那组无法写,分区恢复后少数派的数据被多数派覆盖。这是 CP 系统的正常行为——保证一致性,牺牲可用性。

解法:这不是 bug 而是特性,ZK 就是要强一致。如果业务能接受最终一致,用 Nacos AP 模式或 Eureka,分区时两组都能读写,恢复后合并。但分布式锁场景必须用 CP(ZK),不能脑裂,否则两个 Leader 同时加锁就超卖了。

⑧ FAQ(5 个高频问题)

ZooKeeper 和 Nacos 什么关系?该用哪个?

都是协调服务,但定位不同:ZK 是通用分布式协调(锁/选举/注册都能做),Nacos 专精注册中心+配置中心

新项目选型:注册中心/配置中心 → Nacos(功能全、运维简单、AP/CP 可切换)。分布式锁/Leader 选举 → ZK 或 Redis(Nacos 不适合做锁)。Kafka 元数据 → ZK(Kafka 内置依赖)。ZK 在新项目里用得越来越少,主要是 Kafka 和 Dubbo 老项目在用。

ZK 分布式锁和 Redis 分布式锁怎么选?

核心区别:ZK 锁是 CP(强一致),Redis 锁是 AP(最终一致)

选 ZK 锁:宁可慢也不能错的场景(金融扣款、库存扣减、秒杀)。ZK 不会脑裂,主从切换时锁不会丢。

选 Redis 锁(Redisson):性能优先、能接受极小概率脑裂的场景。Redis 单机 10 万 QPS,ZK 只有 5000-10000。

实际中 90% 场景用 Redis 锁(够用且快),只有金融级强一致才上 ZK 锁。

ZK 集群为什么必须是奇数台?

ZK 用"过半写成功"保证一致性。奇数台的容错能力最优

• 3 台:挂 1 台还能用(2 > 1.5 ✓),挂 2 台废(1 < 1.5 ✗)→ 容错 1 台

• 4 台:挂 1 台还能用(3 > 2 ✓),挂 2 台废(2 = 2 ✗)→ 容错 1 台

• 5 台:挂 2 台还能用(3 > 2.5 ✓)→ 容错 2 台

3 台和 4 台容错能力一样(都只能挂 1 台),但 4 台多花一台钱。所以奇数台性价比最高。生产推荐 3 台(小规模)或 5 台(大规模),不要用偶数台。

临时节点和持久节点有什么区别?什么时候用哪个?

临时节点(EPHEMERAL):生命周期绑定客户端会话,客户端断开自动删除。不能有子节点。用于:注册中心(服务下线自动摘除)、分布式锁(持锁者崩溃自动释放锁)、Leader 选举(Leader 崩溃自动重选)。

持久节点(PERSISTENT):创建后一直存在,需要主动删除。用于:配置数据、目录结构、元数据。

判断标准:"这个节点代表的实体还在不在?实体没了节点该不该自动消失?" 该消失→临时节点,不该消失→持久节点。

ZK 的 Watch 为什么是一次性的?怎么解决?

为什么一次性:避免通知风暴。如果是持久的,一个高频变化的节点会给所有订阅者持续发通知,ZK 扛不住。一次性保证每个事件精确通知一轮。

怎么解决漏事件:

① 收到通知后先重新注册 Watch 再处理数据(顺序很重要);

② ZK 3.6+ 用 addPersistentWatch()(持久 Watch,自动重新注册);

③ 用 Curator 的 NodeCache / PathChildrenCache(封装了重新注册逻辑,不会漏);

配置中心场景直接用 Nacos,不用自己处理 Watch 问题。

✅ ZooKeeper 三句话速记

1. 定位:分布式协调服务,不是数据库。存元数据和协调状态,不存业务数据。像分布式系统的"村委会"。

2. 核心机制:临时节点(会话绑定,断了自动删)+ Watch(一次性通知)。这两个组合出锁/选举/注册中心。

3. 现状:新项目用得越来越少。注册中心/配置中心 → Nacos。分布式锁 → ZK 或 Redis。ZK 主要存量在 Kafka 和 Dubbo 老项目。

相关专题:Nacos 专题 · Redis 专题 · API与微服务决策

全栈资料库 · ZooKeeper 分布式协调实战