主题
十二、Zookeeper(5 题)
12.1 ★★★Zookeeper 的数据模型是怎样的?ZNode 有哪些类型?在分布式系统中的典型应用场景有哪些?
📖 点击查看答案
数据模型:
- 类似文件系统的树形层级结构,每个节点称为 ZNode。
- ZNode 既可存储数据(默认 1MB 上限),也可拥有子节点。
- 每个 ZNode 有 Stat 元数据:版本号(version/cversion/aversion)、时间戳、ACL 等。
- 路径唯一,以
/分隔,如/app1/config/db_url。
ZNode 类型(5 种):
| 类型 | 特性 | 场景 |
|---|---|---|
| 持久节点(Persistent) | 客户端断开后仍存在 | 配置存储 |
| 持久顺序节点(Persistent_Sequential) | 持久 + 自动追加单调递增序号 | 分布式锁、队列 |
| 临时节点(Ephemeral) | 会话失效自动删除 | 服务注册、Leader 选举 |
| 临时顺序节点(Ephemeral_Sequential) | 临时 + 顺序序号 | 公平锁 |
| 容器节点(Container, 3.6+) | 子节点都被删时自动清理 | Leader 选举父节点 |
java
// 创建节点示例
zk.create("/config/db_url", "mysql://...".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
zk.create("/lock/node-", data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 返回路径如 /lock/node-00000001典型应用场景:
- 配置中心:节点存配置,客户端 Watch 节点变更,自动更新。
- 服务注册与发现:服务启动创建临时节点
/services/user-service/instance-001,消费端监听子节点变化。 - 分布式锁:基于临时顺序节点实现公平锁。
- Leader 选举:多节点创建临时节点
/leader,成功者为 Leader,其他 Watch 该节点。 - 分布式队列:基于顺序节点实现 FIFO 队列。
- 集群管理:临时节点感知节点上下线。
关键点:ZNode 数据量小(KB 级),不适合存大数据;版本号用于乐观锁(CAS 更新);临时节点的生命周期绑定会话(session),非连接。
12.2 ★★★ZAB 协议(原子广播)的原理是什么?它和 Paxos 有什么关系/区别?Zookeeper 是如何保证 CP 一致性的?
📖 点击查看答案
ZAB 协议(ZooKeeper Atomic Broadcast):专为 ZooKeeper 设计的分布式一致性协议,保证主备数据同步与事务顺序一致。
两个核心阶段:
- 崩溃恢复(Leader 选举):Leader 宕机或集群启动时,选举新 Leader。
- 消息广播(原子广播):Leader 处理客户端写请求,广播给所有 Follower。
消息广播流程(类似两阶段提交):
- Leader 收到写请求,分配全局唯一单调递增的 zxid(64 位:epoch + counter)。
- Leader 为每个 Follower 创建 FIFO 队列,发送 Proposal。
- Follower 收到 Proposal 写入本地日志,返回 ACK。
- Leader 收到过半数 ACK(含自身)后,发送 Commit 给所有 Follower。
- Follower 收到 Commit 后应用事务。
与 Paxos 的关系:
- ZAB 借鉴 Paxos 的"多数派"思想(quorum),但简化设计。
- ZAB 不像 Paxos 那样允许任意节点提议,而是 Leader 独占提议权(强 Leader 模型)。
- ZAB 保证消息顺序(主备顺序一致),Paxos 不保证多提议间的顺序。
区别对比:
| 维度 | ZAB | Paxos |
|---|---|---|
| 角色 | Leader/Follower/Observer | Proposer/Acceptor/Learner |
| 提议权 | Leader 独占 | 任意 Proposer |
| 一致性 | 强一致 + 顺序一致 | 最终一致 |
| 场景 | 主备同步 | 通用共识 |
| 实现 | 简单 | 复杂 |
| 顺序保证 | 严格 FIFO | 不保证 |
CP 一致性保证:
- 写一致性:所有写请求由 Leader 处理,过半 ACK 才提交,保证已提交事务在多数节点持久化。
- 选举恢复:Leader 故障后,新 Leader 必须包含所有已提交的 zxid 最大事务(选举时优先 zxid 大的节点)。
- 顺序一致:客户端看到的事务顺序与 Leader 发起顺序一致。
- 读一致性:默认读可能读到旧数据(Follower 落后),可通过
sync()命令强制同步后再读。
zxid 结构:
zxid = | epoch (32 bit) | counter (32 bit) |
epoch:每次 Leader 切换自增
counter:单 epoch 内事务递增关键点:ZAB 保证的是顺序一致性(CP),不是强一致;选举期间集群不可写(可用性 A 受影响);读默认不保证读到最新,但可通过 sync 解决。
12.3 ★★★★Zookeeper 的 Leader 选举过程是怎样的(myid + zxid 比较)?什么情况下会触发重新选举?
📖 点击查看答案
Leader 选举触发场景:
- 集群初始化启动。
- Leader 宕机或网络隔离。
- 过半 Follower 失联(Leader 无法形成 quorum)。
选举流程(FastLeaderElection):
- 每个节点状态变为 LOOKING,发起投票(初始投自己)。
- 投票信息包含
(myid, zxid),广播给所有节点。 - 收到其他节点投票后,按规则比较:
- 第一轮比较 zxid:zxid 大的优先(数据更新)。
- zxid 相同比较 myid:myid 大的优先(配置文件指定)。
- 若收到的投票优于自己,更新自己的投票并重新广播。
- 若有节点获得过半数投票(
n/2 + 1),当选 Leader,状态变为 LEADING。 - 其他节点状态变为 FOLLOWING,与 Leader 同步数据。
比较规则详解:
if (peer.zxid > my.zxid) → 投 peer
else if (peer.zxid < my.zxid) → 投自己
else if (peer.myid > my.myid) → 投 peer
else → 投自己示例:
- 3 节点集群:Node1(myid=1, zxid=100)、Node2(myid=2, zxid=100)、Node3(myid=3, zxid=99)。
- Node1 投 (1, 100),Node2 投 (2, 100),Node3 投 (3, 99)。
- Node1 收到 Node2 的 (2,100):zxid 相同,myid 大,更新为 (2,100)。
- Node3 收到 Node2 的 (2,100):zxid 大,更新为 (2,100)。
- Node2 收到过半投票 (2,100),当选 Leader。
选举后的数据同步:
- Follower 发送自己最大的 zxid 给 Leader。
- Leader 判断差异:
- Follower 落后:发送缺失的 Proposal。
- Follower 有未提交事务(Leader 没有的):要求 Follower 截断(保证 Leader 数据权威)。
- 同步完成后,集群进入正常广播状态。
关键配置:
initLimit:Leader 选举后,Follower 与 Leader 初始同步的最大时间(心跳数,默认 10)。syncLimit:运行时 Follower 与 Leader 心跳超时时间(默认 5)。- 奇数节点:3 个或 5 个,避免脑裂(quorum = n/2 + 1)。
关键点:zxid 优先保证数据最新的节点当选,减少数据丢失;myid 仅作 tie-breaker;选举期间集群不可用(CP 牺牲 A);过半原则保证选举结果唯一。
12.4 ★★★★Zookeeper 的 Watcher 监听机制原理和特点是什么?为什么是一次性的?有什么局限性?
📖 点击查看答案
Watcher 原理:
- 客户端注册 Watcher 时,服务端将 Watcher 信息存入
WatchTable(按 path 索引)。 - 客户端本地也存一份(
ZKWatchManager),用于连接断开重连后恢复。 - ZNode 发生变更时,服务端查找
WatchTable,向对应客户端发送事件通知。 - 客户端按 path 找到本地 Watcher,执行回调
process(WatchedEvent)。
Watcher 事件类型:
NodeCreated:节点创建。NodeDeleted:节点删除。NodeDataChanged:节点数据变更。NodeChildrenChanged:子节点变更(仅子节点增减,子节点数据变更不触发)。ChildWatchRemoved/DataWatchRemoved(3.6+):Watcher 被移除。
特点:
- 一次性触发:事件触发后 Watcher 自动失效,需重新注册才能监听下一次变更。
- 客户端串行:同一客户端的所有 Watcher 回调串行执行,避免并发问题。
- 轻量级:通知只包含事件类型与 path,不包含变更后的数据(客户端需主动
getData)。 - 最终一致:客户端收到通知后再读数据,可能中间又发生变更(需处理版本)。
为什么一次性:
- 简化服务端实现:避免维护复杂的状态机与重复通知。
- 防止通知风暴:网络抖动时频繁变更不致大量通知堆积。
- 客户端收到通知后重新注册,可拿到最新数据,避免数据不一致。
局限性:
- 一次性:事件与重新注册之间有窗口期,可能漏掉中间变更。
- 无数据载荷:需二次 RPC 获取数据,增加延迟。
- 串行执行:回调慢会阻塞后续通知。
- 无法监听孙子节点:仅监听直接子节点。
- session 失效后本地 Watcher 失效:重连后服务端 Watcher 保留,但需客户端
exists重新激活。
3.6+ 永久 Watcher(PersistentWatcher):
java
// 永久监听,触发后不失效
zk.addWatch("/config", watcher, AddWatchMode.PERSISTENT);
// 递归监听所有子节点
zk.addWatch("/app", watcher, AddWatchMode.PERSISTENT_RECURSIVE);最佳实践:
- 使用 Curator 的
NodeCache/PathChildrenCache/TreeCache封装自动重注册。 - 回调中避免耗时操作,异步处理业务逻辑。
java
// 原生 API 示例
Stat stat = zk.exists("/config", event -> {
System.out.println("节点变更:" + event.getType());
// 重新注册
try { zk.exists("/config", this); } catch (Exception ignored) {}
});关键点:一次性是为了简化服务端实现;3.6+ 永久 Watcher 解决重注册痛点;生产推荐 Curator Cache,避免手动管理 Watcher 的复杂性。
12.5 ★★★★基于 Zookeeper 如何实现分布式锁?临时顺序节点的作用是什么?如何避免羊群效应?
📖 点击查看答案
简单实现(非推荐):所有客户端抢占创建同一临时节点 /lock,成功者获取锁,失败者 Watch 该节点。释放锁(删除节点)后,所有等待者被唤醒再次抢占。
问题——羊群效应:
- 锁释放时,所有 N 个等待者同时收到通知,同时尝试创建节点。
- 只有一个成功,其余 N-1 个失败继续等待。
- 大量无效请求打到 ZK,浪费资源,且延迟随 N 增大而增大。
优化实现——临时顺序节点(推荐):
- 客户端在
/lock下创建临时顺序节点,如/lock/node-00000001。 - 获取
/lock下所有子节点,排序。 - 若自己是序号最小的节点 → 获取锁,执行业务。
- 否则,只 Watch 前一个节点(比自己序号小且最大的),当前一个节点删除时,自己成为最小,获取锁。
- 业务完成或会话失效(临时节点自动删除)→ 释放锁,下一个节点被唤醒。
java
// Curator InterProcessMutex 实现(推荐)
InterProcessMutex lock = new InterProcessMutex(client, "/lock");
try {
if (lock.acquire(5, TimeUnit.SECONDS)) {
// 获取锁成功,执行业务
doBusiness();
}
} finally {
lock.release();
}原生实现核心代码:
java
public void lock() throws Exception {
// 1. 创建临时顺序节点
String path = zk.create("/lock/node-", data, OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
// 2. 获取所有子节点并排序
List<String> children = zk.getChildren("/lock", false);
Collections.sort(children);
// 3. 判断自己是否最小
String currentNode = path.substring("/lock/".length());
int index = children.indexOf(currentNode);
if (index == 0) {
return; // 获取锁
}
// 4. 监听前一个节点
String prevNode = children.get(index - 1);
CountDownLatch latch = new CountDownLatch(1);
zk.exists("/lock/" + prevNode, event -> {
if (event.getType() == EventType.NodeDeleted) {
latch.countDown(); // 前一个释放,唤醒自己
}
});
latch.await();
}
public void unlock() throws Exception {
zk.delete(currentPath, -1);
}临时顺序节点的作用:
- 临时:会话失效自动删除,防止死锁(客户端宕机时锁自动释放)。
- 顺序:ZK 保证序号单调递增,确定加锁顺序,实现公平锁。
避免羊群效应的关键:
- 每个等待者只 Watch 前一个节点,而非所有 Watch 同一节点。
- 锁释放时只唤醒一个后继者,而非所有等待者。
- 类似 AQS 的等待队列机制,O(1) 唤醒而非 O(N)。
对比其他分布式锁:
| 维度 | ZK 锁 | Redis 锁 | DB 锁 |
|---|---|---|---|
| 一致性 | CP(强一致) | AP(最终一致) | 强一致 |
| 可用性 | Leader 故障时不可用 | 高 | 依赖 DB |
| 公平性 | 公平锁 | 非公平 | 取决于实现 |
| 性能 | 中(写走 Leader) | 高 | 低 |
| 死锁 | 无(临时节点) | 需 TTL 兜底 | 需超时 |
关键点:临时顺序节点是公平锁的核心;监听前驱节点避免羊群效应;Curator 封装完善(可重入、读写锁),推荐使用;CP 特性适合强一致场景,高并发场景可考虑 Redis 锁。