MingJunDuan的博客
全站访问量

ZooKeeper 3.4.10 集群深度剖析(上)—— ZAB 协议与核心架构

ZooKeeper 作为分布式系统的”协调中枢”,在企业内部几乎无处不在——配置中心、服务发现、分布式锁、Leader 选举、消息队列……这些场景背后都离不开 ZK。但很多中间件团队成员对 ZK 的理解停留在”会调 API”层面,对内部的 ZAB 协议、Session 机制、Leader 选举等核心原理缺乏系统认知。

本文以 ZooKeeper 3.4.10 + 5 节点集群为基准,深入源码层面,覆盖 ZK 的方方面面。上篇聚焦 ZK 的定位、集群架构和 ZAB 协议——这是理解 ZK 一切的基石。

一、ZooKeeper 是什么

1.1 官方定义

ZooKeeper 是一个分布式协调服务,为分布式应用提供高性能、高可用、严格顺序访问的协调能力。它本质上是一个精简的文件系统 + 通知机制

┌──────────────────────────────────────────────────────┐
│                   ZooKeeper 定位                       │
│                                                      │
│  不是一个数据库     不是一个缓存     不是一个消息队列    │
│       ↓                ↓               ↓              │
│  它是分布式系统间的 "协调器" —— 让多个进程达成共识       │
└──────────────────────────────────────────────────────┘

1.2 典型使用场景

                      ┌─────────────┐
                      │             │
         配置管理      │             │   命名服务
       ┌──────────────┤  ZooKeeper  ├──────────────┐
       │              │             │              │
       ▼              └─────────────┘              ▼
  ┌─────────┐                              ┌─────────┐
  │ /config │                              │ /naming │
  │ /db.yml │                              │ /rpc/1  │
  │ /mq.yml │                              │ /rpc/2  │
  └─────────┘                              └─────────┘

       │                                            │
       ▼              ┌─────────────┐              ▼
  分布式锁             │             │          Leader 选举
  ┌──────────┐        │  ZooKeeper  │       ┌──────────┐
  │ /lock/A  │◄───────┤             ├───────►│ /election│
  │ EPHEMERAL│        │             │       │ SEQUENTIAL│
  └──────────┘        └─────────────┘       └──────────┘
  • 配置管理/config/app/db.url,变更时 Watcher 通知所有客户端
  • 命名服务/services/order-service/192.168.1.10:8080(Dubbo 经典用法)
  • 分布式锁:临时顺序节点 + Watch 前一个节点
  • Leader 选举:EPHEMERAL_SEQUENTIAL 节点,序号最小者当选
  • 队列:顺序节点实现 FIFO 队列(不推荐,ZK 不适合作高吞吐存储)

1.3 CAP 模型下的 ZK

在 CAP 三角中,ZK 明确选择了 CP(一致性 + 分区容错),牺牲了可用性:

          C (Consistency)
          /\
         /  \
        / ZK \
       /______\
      /        \
     /          \
    A            P
(Availability)  (Partition Tolerance)

ZK = CP 系统
- 发生网络分区时,少数派节点不可用(拒绝服务),保证数据一致
- 这也是为什么 ZK 集群推荐奇数台(过半机制)

这意味着:当 5 节点集群中 3 台故障时,剩余 2 台无法形成过半,整个集群不可用。这是 ZK 的核心设计取舍。


二、集群架构总览 —— 5 节点全景图

2.1 物理拓扑

┌─────────────────────────────────────────────────────────────┐
│                    ZooKeeper 5节点集群                        │
│                                                             │
│                    ┌─────────┐                              │
│                    │  Leader  │  (zk1: 10.0.0.1:2181)       │
│                    │  myid=1  │                              │
│                    └────┬────┘                              │
│                         │                                   │
│            ┌────────────┼────────────┐                      │
│            │            │            │                      │
│            ▼            ▼            ▼                      │
│       ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐     │
│       │Follower │ │Follower │ │Follower │ │Observer │     │
│       │ myid=2  │ │ myid=3  │ │ myid=4  │ │ myid=5  │     │
│       │10.0.0.2 │ │10.0.0.3 │ │10.0.0.4 │ │10.0.0.5 │     │
│       └─────────┘ └─────────┘ └─────────┘ └─────────┘     │
│            ▲            ▲            ▲                     │
│            │            │            │                     │
│            └────────────┼────────────┘                     │
│                         │                                   │
│                    ┌────┴────┐                              │
│                    │ Clients │ (Java / C / Python ...)     │
│                    └─────────┘                              │
└─────────────────────────────────────────────────────────────┘

角色说明(3.4.10):

角色 职责 参与投票 参与写 直接读
Leader 处理写请求、发起 Proposal、协调投票、管理 Session
Follower 转发写请求到 Leader、参与投票、处理读请求
Observer 处理读请求、转发写请求、不参与投票

Observer 的作用:在不降低写性能的前提下水平扩展读能力。每增加一个参与投票的节点,ZAB 的写延迟就增加(需要更多 ACK),但 Observer 不参与投票,所以可以自由扩。

2.2 单节点内部架构

┌─────────────────────────────────────────────────────────────┐
│                    ZK 单节点内部架构                          │
│                                                             │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                   网络层 (NIOServerCnxnFactory)        │  │
│  │         Java NIO Selector + 多线程 Accept/Read/Write  │  │
│  └──────────────────────┬───────────────────────────────┘  │
│                         │                                   │
│                         ▼                                   │
│  ┌──────────────────────────────────────────────────────┐  │
│  │              请求处理器链 (RequestProcessor)           │  │
│  │                                                      │  │
│  │  Leader 链:                                           │  │
│  │  PrepRequest → Proposal → Sync → Ack → Commit → Final│  │
│  │                                                      │  │
│  │  Follower 链:                                         │  │
│  │  FollowerRequest → Commit → Final                    │  │
│  └──────────────────────┬───────────────────────────────┘  │
│                         │                                   │
│                         ▼                                   │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                    数据层                              │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌────────────┐  │  │
│  │  │  DataTree    │  │ SessionTracker│  │  Watcher   │  │  │
│  │  │  (内存树)    │  │ (会话跟踪)    │  │  Manager   │  │  │
│  │  └──────────────┘  └──────────────┘  └────────────┘  │  │
│  └──────────────────────┬───────────────────────────────┘  │
│                         │                                   │
│                         ▼                                   │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                    持久化层                            │  │
│  │  ┌──────────────────┐  ┌────────────────────────┐    │  │
│  │  │ Transaction Log  │  │     Snapshot           │    │  │
│  │  │ (事务日志 WAL)   │  │  (快照 / 数据镜像)     │    │  │
│  │  │ dataDir/version-2│  │ dataDir/version-2      │    │  │
│  │  │ log.xxx          │  │ snapshot.xxx           │    │  │
│  │  └──────────────────┘  └────────────────────────┘    │  │
│  └──────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

2.3 核心数据结构:DataTree

ZK 的内存数据模型是一棵 DataTree,每个节点叫 DataNode

// ZooKeeper 3.4.10 源码: src/java/main/org/apache/zookeeper/server/DataTree.java
public class DataTree {
    // 根节点 "/" 为起点,in-memory 的 ConcurrentHashMap
    private final ConcurrentHashMap<String, DataNode> nodes =
        new ConcurrentHashMap<String, DataNode>();

    // Watch 管理
    private final WatchManager dataWatches;
    private final WatchManager childWatches;

    // 节点变化监听 (用于 Quorum 复制)
    private final ArrayList<ProcessTxnResult> listeners;
}

// 每个 ZNode 的内存表示
public class DataNode implements Record {
    byte[] data;                    // 节点数据
    Long acl;                      // ACL 权限
    StatPersisted stat;            // 持久化状态
    private Set<String> children;  // 子节点名称集合
}

对应到用户可见的路径:

/  (DataNode: children=["config", "services", "locks"])
├── config
│   ├── db (DataNode: data="jdbc:mysql://...")
│   └── mq (DataNode: data="...")
├── services
│   └── order-service (DataNode: ephemeralOwner=sessionId)
└── locks
    └── lock-0000000001 (DataNode: EPHEMERAL_SEQUENTIAL)

关键设计点

  • ZK 将所有数据全量加载在内存中(这也是为什么 ZNode 限制 1MB、不适宜做大容量存储)
  • 节点间的关系通过 children 集合维护,查找子节点是 O(1) HashMap 操作
  • ConcurrentHashMap 保证读操作的并发安全,写操作通过 ZAB 保证全局有序

三、ZAB 协议 —— ZooKeeper 的灵魂

ZAB (ZooKeeper Atomic Broadcast) 是整个 ZK 最核心的协议。它解决了分布式系统中两个根本问题:

  1. Leader 正常工作时:如何高效地将写操作复制到所有节点(原子广播)
  2. Leader 崩溃后:如何选出新 Leader 并让所有节点数据一致(崩溃恢复)

ZAB 不是 Paxos,也不是 Raft。它是 ZooKeeper 为了满足自身需求专门设计的协议。核心诉求是:保证所有副本以相同的顺序执行相同的写操作

3.1 zxid —— 全局事务 ID

理解 ZAB 的第一步是理解 zxid(ZooKeeper Transaction ID)

zxid 是一个 64 位的 Long 整数
┌──────────────────────┬──────────────────────────────────────┐
│   高 32 位: epoch    │       低 32 位: counter               │
│   (Leader 任期编号)   │     (事务递增序号)                    │
└──────────────────────┴──────────────────────────────────────┘

示例:
  epoch=1, counter=0    → zxid = 0x0000000100000000
  epoch=1, counter=1    → zxid = 0x0000000100000001
  ...
  epoch=1, counter=999  → zxid = 0x00000001000003E7
  --- Leader 切换 ---
  epoch=2, counter=0    → zxid = 0x0000000200000000
  epoch=2, counter=1    → zxid = 0x0000000200000001

epoch 的作用

  • 每次选举产生新 Leader,epoch +1
  • 用于区分不同 Leader 任期的事务,防止旧 Leader 的”僵尸”事务污染集群
  • epoch 值越大,代表数据越新(选举时用于比较)

counter 的作用

  • 在同一个 epoch 内单调递增
  • 反映了写操作的全序关系:zxid 越大,操作越晚

源码定义(3.4.10):

// src/java/main/org/apache/zookeeper/server/quorum/QuorumPeer.java
public class QuorumPeer extends ZooKeeperThread {
    // 当前 epoch (每次选举成功后更新)
    long currentEpoch;

    // 已接受的最新 epoch
    long acceptedEpoch;

    // 从快照恢复时获取最新 zxid
    public long getLastLoggedZxid() {
        // 从事务日志中读取最后一条记录的 zxid
    }
}

// zxid 的构造
// src/java/main/org/apache/zookeeper/server/util/ZxidUtils.java
public class ZxidUtils {
    public static long getEpochFromZxid(long zxid) {
        return zxid >> 32;
    }

    public static long getCounterFromZxid(long zxid) {
        return zxid & 0xFFFFFFFFL;
    }

    public static long makeZxid(long epoch, long counter) {
        return (epoch << 32) | (counter & 0xFFFFFFFFL);
    }
}

3.2 原子广播 —— 一个写请求的完整旅程

这是 ZAB 协议最核心的流程。假设 Client 连接在 Follower-2 上,发起一个 create /config/app {"key":"value"} 请求:

时间线
  │
  ▼
  Client          Follower-2 (myid=2)        Leader (myid=1)      Follower-3,4 (myid=3,4)
  ─────          ──────────────────          ──────────────      ──────────────────────
  │                                           ① Proposal
  │  1.写请求                                  ┌──────────┐
  ├──────────────►│                           │ 生成zxid │
  │               │  2.转发请求                 │ 写入日志 │
  │               ├──────────────────────────►│ 构造Proposal│
  │               │                           └────┬─────┘
  │               │                                │
  │               │       ② 广播 Proposal           │
  │               │◄───────────────────────────────┤
  │               │◄───────────────────────────────┼──────────────────►│
  │               │                                │                   │
  │               │       ③ Follower 写日志+ACK     │                   │
  │               │   ┌──────────┐                 │   ┌──────────┐   │
  │               │   │写事务日志  │                 │   │写事务日志  │   │
  │               │   │写入内存   │                 │   │写入内存   │   │
  │               │   │(可选)    │                 │   │(可选)    │   │
  │               │   └────┬─────┘                 │   └────┬─────┘   │
  │               │        │ ACK                   │        │ ACK     │
  │               │        ├──────────────────────►│        │────────►│
  │               │        │                       │                  │
  │               │        │   ④ 过半 ACK → Commit  │                  │
  │               │        │         ┌──────────┐   │                  │
  │               │        │         │ 统计票数  │   │                  │
  │               │        │         │ >= 半数?  │   │                  │
  │               │        │         └────┬─────┘   │                  │
  │               │        │              │          │                  │
  │               │        │  ⑤ 广播 Commit         │                  │
  │               │◄───────┼────────────────────────┤                  │
  │               │◄───────┼────────────────────────┼─────────────────►│
  │               │        │                        │                  │
  │               │  ⑥ 应用数据到内存                 │  ⑥ 应用数据到内存│
  │               │  ┌──────────┐                    │  ┌──────────┐  │
  │               │  │写入DataTree│                   │  │写入DataTree│  │
  │               │  └──────────┘                    │  └──────────┘  │
  │               │        │                        │                  │
  │  ⑦ 返回结果    │        │                        │                  │
  │◄──────────────┤        │                        │                  │
  │               │        │                        │                  │

关键源码走读(Leader 端 Proposal 流程)

// src/java/main/org/apache/zookeeper/server/quorum/Leader.java
public class Leader {
    // 已提交的 Proposal 队列,用于 Follower 同步
    ConcurrentLinkedQueue<Proposal> outstandingProposals;

    /**
     * 发起 Proposal:Leader 收到写请求后调用
     */
    public Proposal propose(Request request) {
        // 1. 生成 zxid
        long zxid = (epoch << 32) | (++lastProposed);

        // 2. 构造 Proposal 包
        Proposal p = new Proposal();
        p.packet = new QuorumPacket(NEWLEADER, zxid, ...);
        p.request = request;

        // 3. 写入本地事务日志
        request.zxid = zxid;
        request.hdr = new TxnHeader(sessionId, ..., zxid, ...);
        txnLog.append(request);

        // 4. 广播 Proposal 给所有 Follower
        for (QuorumServer server : self.getVotingView().values()) {
            QuorumPacket qp = new QuorumPacket(PROPOSAL, zxid, ...);
            server.write(qp);
        }
        return p;
    }

    /**
     * 收到 Follower 的 ACK 后,检查是否过半
     */
    synchronized public void processAck(long sid, long zxid, ...) {
        Proposal p = outstandingProposals.get(zxid);
        p.acks.add(sid);

        // 核心判断:ACK 数量 >= 半数 ?
        if (p.acks.size() >= self.getQuorumVerifier().getVotingMembers().size() / 2) {
            // 过半!发起 Commit
            commit(zxid);
        }
    }
}

为什么是”过半”而不是”全部”?

5节点集群,过半 = ceil(5/2) = 3

情况 1:收到 3 个 ACK(含自己)→ Commit ✓
情况 2:只收到 2 个 ACK → 等待(可能超时触发新一轮选举)

"过半"保证了:
- 任意两次"过半"的集合一定有交集
- 交集节点知道上一次 Commit 的状态
- 新 Leader 一定来自"过半"的交集 → 拥有最新数据

3.3 ZAB 的流水线化 —— 为什么比朴素 2PC 快

朴素的 2PC(Two-Phase Commit)是串行的:Proposal → Commit → 下一个 Proposal → Commit ……

ZAB 做了关键优化——流水线化

朴素 2PC (串行):
│ Proposal#1 │ Commit#1 │ │ Proposal#2 │ Commit#2 │ │ Proposal#3 │ Commit#3 │
─────────────────────────────────────────────────────────────────────────────► 时间

ZAB (流水线化):
│ Proposal#1 │ Proposal#2 │ Proposal#3 │ Commit#1 │ Commit#2 │ Commit#3 │
─────────────────────────────────────────────────────────────────────────────► 时间

关键差异:
- 不需要等 Commit#1 完成才 Proposal#2
- 多个 Proposal 可以同时在网络中飞行
- 提高吞吐量约 30%-50%(取决于网络延迟)

但 ZAB 保证了一个关键约束:Commit 消息必须按 zxid 顺序发送。即使 Proposal#3 先于 Proposal#2 到达 Follower,Leader 也必须保证 Commit#2 在 Commit#3 之前发送。

源码体现:

// Leader.java: commit() 方法保证顺序
synchronized public void commit(long zxid) {
    Proposal p = outstandingProposals.get(zxid);
    // 关键:通过 lastCommitted 追踪已提交的最大 zxid
    if (zxid != lastCommitted + 1) {
        // 有更小的 zxid 还没 Commit,必须等!
        pendingCommits.add(p);
        return;
    }

    // 可以按序提交
    doCommit(p);
    lastCommitted = zxid;

    // 检查 pendingCommits 有没有可以提交的
    while (!pendingCommits.isEmpty()) {
        Proposal next = pendingCommits.peek();
        if (next.packet.getZxid() == lastCommitted + 1) {
            pendingCommits.poll();
            doCommit(next);
            lastCommitted = next.packet.getZxid();
        } else {
            break;
        }
    }
}

3.4 Follower 与 Leader 的请求处理器链

Leader 的处理器链

                         ┌──────────────────┐
                         │     Network      │
                         │  (Client/其他节点) │
                         └────────┬─────────┘
                                  │
                                  ▼
                    ┌─────────────────────────┐
                    │  PrepRequestProcessor   │
                    │  - 生成 TxnHeader       │
                    │  - 设置 sessionId       │
                    │  - 分配 zxid            │
                    └────────┬────────────────┘
                                  │
                                  ▼
                    ┌─────────────────────────┐
                    │ ProposalRequestProcessor │
                    │  - Leader.propose()     │
                    │  - 广播 Proposal         │
                    │  - 等待 ACK             │
                    └────────┬────────────────┘
                                  │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       ▼
          ┌──────────────────┐    ┌──────────────────┐
          │ SyncRequest      │    │ AckRequest       │
          │ Processor        │    │ Processor        │
          │ - 写事务日志      │    │ - 处理 Follower   │
          │ - 生成快照(条件)  │    │   的 ACK         │
          └──────────────────┘    │ - 判断过半→Commit │
                                  └────────┬─────────┘
                                           │
                                           ▼
                                ┌─────────────────────┐
                                │  CommitProcessor    │
                                │  - 保证按序提交      │
                                │  - 协调 pendingCommits│
                                └────────┬────────────┘
                                         │
                                         ▼
                                ┌─────────────────────┐
                                │ FinalRequestProcessor│
                                │  - 应用更改到 DataTree │
                                │  - 触发 Watcher      │
                                │  - 返回结果给 Client  │
                                └─────────────────────┘

Follower 的处理器链

                         ┌──────────────────┐
                         │     Network      │
                         └────────┬─────────┘
                                  │
                    ┌─────────────┴─────────────┐
                    │                           │
                    ▼                           ▼
          ┌──────────────────┐    ┌──────────────────────┐
          │FollowerRequest   │    │  CommitProcessor     │
          │Processor         │    │  (处理来自 Leader    │
          │                  │    │   的 Commit 消息)     │
          │ - 读请求:直接处理 │    └──────────┬───────────┘
          │ - 写请求:转发到  │               │
          │   Leader        │               ▼
          └──────────────────┘    ┌──────────────────────┐
                                  │ FinalRequestProcessor│
                                  │ - 应用 DataTree      │
                                  │ - 触发 Watcher       │
                                  │ - 返回结果           │
                                  └──────────────────────┘

Follower 处理写请求时的转发逻辑(源码):

// src/java/main/org/apache/zookeeper/server/quorum/FollowerRequestProcessor.java
public class FollowerRequestProcessor extends ZooKeeperCriticalThread
        implements RequestProcessor {

    @Override
    public void processRequest(Request request) {
        if (request.isRead()) {
            // 读请求:直接交给 CommitProcessor → FinalRequestProcessor
            nextProcessor.processRequest(request);
        } else {
            // 写请求:必须转发给 Leader
            request.setStale(); // 标记为待转发
            // 通过 Learner 的链接发送给 Leader
            zks.getFollower().request(request);
        }
    }
}

3.5 ACK 的语义

Follower 发送 ACK 的含义是:我已经把这个 Proposal 持久化到事务日志了。这是一个非常重要的承诺。

// src/java/main/org/apache/zookeeper/server/quorum/Learner.java
// Follower/Observer 的基类,处理来自 Leader 的 Proposal
protected void processPacket(QuorumPacket qp) {
    switch (qp.getType()) {
        case Leader.PROPOSAL:
            // 收到 Proposal
            TxnHeader hdr = new TxnHeader();
            Record txn = SerializeUtils.deserializeTxn(qp.getData());

            // 关键:先写事务日志!
            zk.getZKDatabase().append(hdr, txn);

            // 再发 ACK
            QuorumPacket ack = new QuorumPacket(Leader.ACK, hdr.getZxid(), null);
            leaderOs.writeRecord(ack, null);
            break;

        case Leader.COMMIT:
            // 收到 Commit:应用到内存 DataTree
            zk.getZKDatabase().commit();
            break;
    }
}

为什么 ACK 前必须先写日志?

如果 Follower 先发 ACK 再写日志:
  1. Follower ACK ✓
  2. Leader 收到过半 ACK → Commit
  3. Follower 在写日志前崩溃了
  4. Follower 重启后,它已经 ACK 了但没有这条数据
  5. Leader 认为它已经有了,不再同步
  → 数据丢失!

正确做法(Follower 先写日志再 ACK):
  1. Follower 写事务日志 ✓
  2. Follower 发送 ACK
  3. Follower 在 Commit 前崩溃
  4. Follower 重启后,日志中有这条记录但没 Commit
  5. 与 Leader 重新同步时,Leader 会告诉它 Commit 或回滚
  → 数据安全

四、崩溃恢复 —— 当 Leader 挂了

Leader 崩溃后,ZAB 进入崩溃恢复模式,做完三件事后回到广播模式

  1. 选举新 Leader
  2. 数据同步(让所有节点追平到新 Leader 的状态)
  3. 恢复广播
                      ┌─────────────┐
                      │ 广播模式     │
                      │ (正常工作)   │
                      └──┬──────┬───┘
                         │      │
              Leader崩溃  │      │  新Leader就绪+数据同步完成
                         │      │
                         ▼      │
                      ┌─────────┴───┐
                      │ 崩溃恢复模式 │
                      │ 1.选举Leader │
                      │ 2.数据同步   │
                      └─────────────┘

4.1 Leader 活跃性检测

ZooKeeper 3.4.10 中,Follower 通过心跳检测 Leader 是否存活:

// QuorumPeer.java 中的 main loop
@Override
public void run() {
    while (running) {
        switch (getPeerState()) {
        case LOOKING:
            // 当前无主,参与选举
            setCurrentVote(makeLEStrategy().lookForLeader());
            break;

        case FOLLOWING:
            // 作为 Follower 运行
            setFollower(makeFollower(logFactory));
            follower.followLeader();
            break;

        case LEADING:
            // 作为 Leader 运行
            setLeader(makeLeader(logFactory));
            leader.lead();
            break;
        }
    }
}

Follower 的 followLeader() 方法中,如果发现与 Leader 的连接断开且超过 syncLimit * tickTime,Follower 会认为 Leader 已经挂了,将自己的状态改回 LOOKING,发起新一轮选举。

4.2 恢复后的数据同步(预告)

数据同步是 ZK 保证一致性的关键环节,三种策略:

  • SNAP(全量同步):Follower 落后太多,Leader 发整个 DataTree 快照
  • DIFF(增量同步):Follower 和 Leader 的差距在 Leader 的缓存范围内,只同步差异 Proposal
  • TRUNC(截断):Follower 比 Leader 还新(上一轮 Commit 未完成),截断多余的

详细的数据同步过程将在中篇和下篇展开。


上篇小结

上篇核心要点回顾:

1. ZK 定位:分布式协调服务,CP 系统,不是数据库/缓存/消息队列
2. 集群角色:Leader(写+协调)、Follower(参与投票+读)、Observer(仅读)
3. 数据模型:DataTree + DataNode,全量内存存储,ConcurrentHashMap
4. ZAB 原子广播:
   - zxid = epoch(32bit) + counter(32bit)
   - 写流程:Proposal → ACK(过半) → Commit(支持流水线化)
   - ACK 前必须写事务日志(保证不丢数据)
5. 崩溃恢复:Leader 挂 → 选举 → 同步 → 恢复广播

接下来中篇将深入 ZK 的数据模型细节、Session 机制(这是理解 ZK 客户端行为的基石)和 Watcher 通知机制。

本文阅读量