Skip to content

十二、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

典型应用场景

  1. 配置中心:节点存配置,客户端 Watch 节点变更,自动更新。
  2. 服务注册与发现:服务启动创建临时节点 /services/user-service/instance-001,消费端监听子节点变化。
  3. 分布式锁:基于临时顺序节点实现公平锁。
  4. Leader 选举:多节点创建临时节点 /leader,成功者为 Leader,其他 Watch 该节点。
  5. 分布式队列:基于顺序节点实现 FIFO 队列。
  6. 集群管理:临时节点感知节点上下线。

关键点:ZNode 数据量小(KB 级),不适合存大数据;版本号用于乐观锁(CAS 更新);临时节点的生命周期绑定会话(session),非连接。


12.2 ★★★ZAB 协议(原子广播)的原理是什么?它和 Paxos 有什么关系/区别?Zookeeper 是如何保证 CP 一致性的?

📖 点击查看答案

ZAB 协议(ZooKeeper Atomic Broadcast):专为 ZooKeeper 设计的分布式一致性协议,保证主备数据同步与事务顺序一致。

两个核心阶段

  1. 崩溃恢复(Leader 选举):Leader 宕机或集群启动时,选举新 Leader。
  2. 消息广播(原子广播):Leader 处理客户端写请求,广播给所有 Follower。

消息广播流程(类似两阶段提交)

  1. Leader 收到写请求,分配全局唯一单调递增的 zxid(64 位:epoch + counter)。
  2. Leader 为每个 Follower 创建 FIFO 队列,发送 Proposal。
  3. Follower 收到 Proposal 写入本地日志,返回 ACK。
  4. Leader 收到过半数 ACK(含自身)后,发送 Commit 给所有 Follower。
  5. Follower 收到 Commit 后应用事务。

与 Paxos 的关系

  • ZAB 借鉴 Paxos 的"多数派"思想(quorum),但简化设计。
  • ZAB 不像 Paxos 那样允许任意节点提议,而是 Leader 独占提议权(强 Leader 模型)。
  • ZAB 保证消息顺序(主备顺序一致),Paxos 不保证多提议间的顺序。

区别对比

维度ZABPaxos
角色Leader/Follower/ObserverProposer/Acceptor/Learner
提议权Leader 独占任意 Proposer
一致性强一致 + 顺序一致最终一致
场景主备同步通用共识
实现简单复杂
顺序保证严格 FIFO不保证

CP 一致性保证

  1. 写一致性:所有写请求由 Leader 处理,过半 ACK 才提交,保证已提交事务在多数节点持久化。
  2. 选举恢复:Leader 故障后,新 Leader 必须包含所有已提交的 zxid 最大事务(选举时优先 zxid 大的节点)。
  3. 顺序一致:客户端看到的事务顺序与 Leader 发起顺序一致。
  4. 读一致性:默认读可能读到旧数据(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 选举触发场景

  1. 集群初始化启动。
  2. Leader 宕机或网络隔离。
  3. 过半 Follower 失联(Leader 无法形成 quorum)。

选举流程(FastLeaderElection)

  1. 每个节点状态变为 LOOKING,发起投票(初始投自己)。
  2. 投票信息包含 (myid, zxid),广播给所有节点。
  3. 收到其他节点投票后,按规则比较:
    • 第一轮比较 zxid:zxid 大的优先(数据更新)。
    • zxid 相同比较 myid:myid 大的优先(配置文件指定)。
  4. 若收到的投票优于自己,更新自己的投票并重新广播。
  5. 若有节点获得过半数投票n/2 + 1),当选 Leader,状态变为 LEADING
  6. 其他节点状态变为 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。

选举后的数据同步

  1. Follower 发送自己最大的 zxid 给 Leader。
  2. Leader 判断差异:
    • Follower 落后:发送缺失的 Proposal。
    • Follower 有未提交事务(Leader 没有的):要求 Follower 截断(保证 Leader 数据权威)。
  3. 同步完成后,集群进入正常广播状态。

关键配置

  • 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 被移除。

特点

  1. 一次性触发:事件触发后 Watcher 自动失效,需重新注册才能监听下一次变更。
  2. 客户端串行:同一客户端的所有 Watcher 回调串行执行,避免并发问题。
  3. 轻量级:通知只包含事件类型与 path,不包含变更后的数据(客户端需主动 getData)。
  4. 最终一致:客户端收到通知后再读数据,可能中间又发生变更(需处理版本)。

为什么一次性

  • 简化服务端实现:避免维护复杂的状态机与重复通知。
  • 防止通知风暴:网络抖动时频繁变更不致大量通知堆积。
  • 客户端收到通知后重新注册,可拿到最新数据,避免数据不一致。

局限性

  1. 一次性:事件与重新注册之间有窗口期,可能漏掉中间变更。
  2. 无数据载荷:需二次 RPC 获取数据,增加延迟。
  3. 串行执行:回调慢会阻塞后续通知。
  4. 无法监听孙子节点:仅监听直接子节点。
  5. 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 增大而增大。

优化实现——临时顺序节点(推荐)

  1. 客户端在 /lock 下创建临时顺序节点,如 /lock/node-00000001
  2. 获取 /lock 下所有子节点,排序。
  3. 若自己是序号最小的节点 → 获取锁,执行业务。
  4. 否则,只 Watch 前一个节点(比自己序号小且最大的),当前一个节点删除时,自己成为最小,获取锁。
  5. 业务完成或会话失效(临时节点自动删除)→ 释放锁,下一个节点被唤醒。
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 锁。

基于 4 份主流面试题库整合精选,共 200 题