第七章 ZooKeeper 技术内幕(一)
7.1 系统模型 (System Model)
ZooKeeper 的系统模型是理解其所有功能的基础。它定义了 ZooKeeper 如何组织数据、节点有哪些特性,以及它如何保证并发操作的正确性和安全性。
7.1.1 数据模型 (Data Model)
ZooKeeper 的核心数据结构非常直观,它是一个类似于标准文件系统的树形结构。
- 树状命名空间 (Namespace):整个数据模型由一个根节点
/开始,下面可以有任意多的子节点。每个节点都通过一个以/分隔的路径来唯一标识,例如/app1/config/db-url。 - ZNode: 树中的每一个节点被称为 ZNode。可以把它想象成一个既像文件又像目录的东西。
- 像文件:每个 ZNode 自身可以存储数据(Data)。这个数据大小有限制,通常是 1MB。存储的数据需要开发者自行序列化和反序列化(例如,存入 JSON 字符串或二进制数据)。
- 像目录:每个 ZNode 都可以拥有子节点。
结构图示例:
/ (根节点)
|
+-----+------+
| |
app1 app2
| |
+---+---+ +---+---+
| | | |
config tasks workers locks
|
+----------+
| |
db-url thread-pool-size
在这个模型中,/app1 是 / 的子节点,/app1/config 是 /app1 的子节点。每个这样的节点都可以存储配置信息、状态数据等。这种层次化的结构对于组织分布式应用中的各种元数据非常方便。
7.1.2 节点特征 (Node Characteristics)
ZNode 不仅仅是一个数据容器,它还具有多种类型和生命周期,这是实现各种复杂分布式协调场景的关键。ZNode 主要分为两大类:持久节点 (Persistent) 和 临时节点 (Ephemeral),每一类又可以加上顺序 (Sequential) 属性。
-
持久节点 (Persistent Node)
- 生命周期:一旦被创建,就一直存在于 ZooKeeper 中,直到有客户端明确地调用删除命令将其删除。它与创建它的客户端的会话(Session)无关。
- 用途:非常适合存储需要长期保存的配置信息、ACL 权限设置、任务队列的根节点等。
-
临时节点 (Ephemeral Node)
- 生命周期:与创建它的客户端会话绑定。如果客户端会话保持活动,节点就存在。一旦会话因客户端主动关闭、崩溃或网络超时而结束,该临时节点会被 ZooKeeper 服务器自动删除。
- 关键约束:临时节点不能拥有子节点。
- 用途:这是实现心跳检测、集群成员管理、Master 选举和分布式锁等功能的核心。例如,服务器实例在启动时创建一个临时节点来“报活”,当它宕机时节点自动消失,其他系统就能立刻感知到。
-
持久顺序节点 (Persistent Sequential Node)
- 特性:具备持久节点的特性,同时在创建时,ZooKeeper 会自动在节点名称后面追加一个单调递增的10位数字序号。
- 例如:如果客户端尝试创建路径
/queue/task-,ZooKeeper 可能会创建出/queue/task-0000000001,下一个创建的则是/queue/task-0000000002。 - 用途:非常适合实现分布式队列,保证任务的先进先出(FIFO)顺序。
-
临时顺序节点 (Ephemeral Sequential Node)
- 特性:结合了临时节点和顺序节点的特性。生命周期与会话绑定,并且节点名带有自动生成的序号。
- 用途:这是实现公平分布式锁和 Master 选举的“黄金搭档”。每个想获取锁或参与选举的客户端都创建一个临时顺序节点,序号最小的那个获得锁或当选 Master。因为是临时节点,所以能有效防止死锁或“脑裂”。
7.1.3 版本 (Version) - 保证分布式数据原子性操作
为了解决分布式环境下的并发更新问题,ZooKeeper 引入了乐观锁机制,其实现依赖于每个 ZNode 的版本号。
每个 ZNode 在创建时,都带有一系列的版本号信息,其中最重要的三个是:
version:当前 ZNode 的数据内容的版本号。每次对此节点的数据进行更新时,version的值会加 1。cversion:当前 ZNode 的子节点列表的版本号。每当对此节点的子节点进行创建或删除时,cversion的值会加 1。aversion:当前 ZNode 的 ACL(访问控制列表) 的版本号。每当对此节点的 ACL 信息进行修改时,aversion的值会加 1。
如何实现原子性操作?
ZooKeeper 的所有更新操作(setData, delete)都是有条件的 (Conditional)。API 中允许客户端在发起更新请求时,提供一个期望的版本号。
- 操作流程:
- 客户端首先读取一个 ZNode 的数据及其当前的
version(例如,v=5)。 - 客户端在本地基于这些数据进行计算或处理。
- 当客户端想将计算后的新数据写回 ZNode 时,它会发起一个
setData(path, data, version)请求,其中version参数传入它当初读取到的版本号5。 - ZooKeeper 服务器在处理这个请求时,会先比较客户端提供的版本号
5和 ZNode 在服务器上当前实际的版本号。- 如果两者一致,说明从客户端读取到写入的这段时间内,没有其他客户端修改过这个 ZNode。服务器接受更新,将新数据写入,并把
version加 1(变为6)。操作成功。 - 如果两者不一致(例如,服务器上的版本已经是
6),说明有“并发冲突”。服务器会拒绝本次更新操作,并返回一个错误。
- 如果两者一致,说明从客户端读取到写入的这段时间内,没有其他客户端修改过这个 ZNode。服务器接受更新,将新数据写入,并把
- 操作失败的客户端需要重新执行第一步(重新读取、重新计算、重新尝试写入),这正是典型的CAS (Compare-And-Swap) 原子操作。
- 客户端首先读取一个 ZNode 的数据及其当前的
作用:
这种基于版本的 CAS 机制,有效地保证了数据更新的原子性,避免了分布式环境下的“丢失更新”问题,是实现分布式数据一致性的重要基石。
7.1.4 Watcher - 数据变更的通知
Watcher(事件监听器)是 ZooKeeper 实现分布式协调的核心机制。它允许客户端在一个 ZNode 上注册一个监听器,当该 ZNode 发生特定类型的变化时,ZooKeeper 服务器会异步地向该客户端发送一个通知。
核心特性:
-
一次性触发 (One-time Trigger)
- 一个 Watcher 在被触发一次之后,就会立即失效。
- 如果客户端想继续监听后续的变化,就必须在收到通知并处理完后,重新注册一个新的 Watcher。这个“读取数据 -> 注册 Watcher”的循环是标准的编程范式。
-
异步发送
- ZooKeeper 服务器发送通知的过程是异步的,不会阻塞服务器对其他请求的处理。它能保证通知会最终送达客户端,但不能保证通知的实时性。
-
事件封装
- Watcher 事件是一个简单的通知,它包含了三部分信息:
- 通知状态 (KeeperState):如
SyncConnected(连接成功),Disconnected(断开连接) 等。 - 事件类型 (EventType):如
NodeCreated,NodeDeleted,NodeDataChanged,NodeChildrenChanged。 - 节点路径 (Path):发生变化的 ZNode 的路径。
- 通知状态 (KeeperState):如
- 重要:Watcher 通知只告诉客户端“发生了什么事”,但不会附带变化后的新数据。例如,
NodeDataChanged事件只告诉你数据变了,但不会告诉你变成了什么。客户端需要自己重新去getData获取最新数据。
- Watcher 事件是一个简单的通知,它包含了三部分信息:
工作机制:
- 客户端在读取操作(如
exists,getData,getChildren)时,可以附带一个 Watcher 对象。 - 这个 Watcher 会被注册到 ZooKeeper 服务器上,并与客户端的会话关联。
- 当 ZNode 发生相应变化时,服务器会将通知发送到客户端的事件处理线程中,触发回调。
用途:
Watcher 机制是实现数据发布/订阅、集群管理、Master 选举等场景中“实时感知变化”功能的基础。
7.1.5 ACL - 保证数据的安全
ACL (Access Control Lists) 是 ZooKeeper 提供的一套权限控制机制,用于保护 ZNode 不被未经授权的客户端访问和修改。
ACL 的组成 (Scheme:ID:Permission)
一个完整的 ACL 权限设置由三部分组成:
-
授权策略 (Scheme):指定了使用哪种方式来识别客户端。
world:开放模式,任何人都可以访问。它只有一个 ID,即anyone。auth:代表任何已经通过认证的用户(不指定具体用户)。digest:最常用的方式,使用username:password形式的凭证来识别客户端。密码会以 SHA-1 加密后传输。ip:使用客户端的 IP 地址作为识别方式。super:超级管理员模式,在此模式下可以对任意 ZNode 进行任何操作。
-
授权对象 (ID):
- 与 Scheme 对应,代表被授权的用户、IP 地址等。例如,对于
digest策略,ID 就是具体的username。
- 与 Scheme 对应,代表被授权的用户、IP 地址等。例如,对于
-
权限 (Permission):
CREATE(c): 允许在当前节点下创建子节点。READ(r): 允许读取当前节点的数据和子节点列表。WRITE(w): 允许设置当前节点的数据。DELETE(d): 允许删除当前节点的子节点。ADMIN(a): 允许设置当前节点的 ACL 权限。- 权限可以单个或组合使用,例如
crwda代表所有权限。
ACL 的特性:
- 权限不继承:ACL 的权限效力仅限于当前节点,不会被子节点继承。如果希望一个目录下的所有节点都有相同的权限,需要为每个节点单独设置。
- 独立性:每个节点的 ACL 都是独立的。
用途:
在多租户或有安全要求的生产环境中,ACL 是必不可少的。例如,可以确保只有 app1 的进程可以修改 /app1/config 下的配置,而 app2 的进程只能读取,从而实现资源隔离和安全访问。
7.2 序列化与协议 (Serialization & Protocol)
在分布式系统中,网络通信是基础。为了在网络上传输数据,我们需要将内存中的对象转换成二进制字节流(序列化),接收方再将字节流转换回对象(反序列化)。同时,通信双方需要遵守一个共同的约定,即通信协议,来解析这些字节流。ZooKeeper 设计了自己的序列化框架 Jute 和一套简洁的通信协议来满足其高性能、跨语言的需求。
7.2.1 Jute 介绍
Jute 是 ZooKeeper 使用的序列化组件。它不是一个像 JSON 或 Protobuf 那样的通用序列化框架,而是专门为 ZooKeeper 定制的代码生成器。
为什么不用现成的?
在 ZooKeeper 设计的年代,像 Protobuf、Thrift 这样的高效序列化框架还未普及或成熟。Java 自带的序列化(java.io.Serializable)存在以下问题:
- 性能较差:序列化后的体积较大,处理速度慢。
- 语言绑定:仅限于 Java 环境,无法实现跨语言支持(ZooKeeper 有 C、Python 等客户端)。
- 版本兼容性问题:类的变更(如增删字段)容易导致反序列化失败。
因此,ZooKeeper 团队开发了 Jute,旨在提供一个高性能、跨语言、易于版本控制的序列化解决方案。
Jute 的核心思想:
Jute 的核心是一个代码生成器。开发者首先需要编写一个 .jute 文件,这个文件定义了需要被序列化的数据结构(在 ZooKeeper 中称为 "record")。然后,Jute 编译器会读取这个文件,并自动生成对应语言(如 Java 或 C)的源代码。
生成的代码包含了:
- 数据结构的类定义(如 Java 的 class)。
- 构造函数、getter/setter 方法。
serialize()和deserialize()方法,用于实现对象和字节流之间的转换。
这种方式将序列化的逻辑编译时就确定下来,而不是在运行时通过反射等方式进行,因此性能非常高。
7.2.2 使用 Jute 进行序列化
我们通过一个例子来理解 Jute 的工作流程。
第一步:定义 .jute 文件
假设我们要定义一个表示会话连接请求的数据结构。我们可以创建一个 ConnectRequest.jute 文件,内容如下:
// 定义一个模块(对应 Java 的 package)
module org.apache.zookeeper.proto {
// 定义一个 record(对应 Java 的 class)
class ConnectRequest {
int protocolVersion; // 协议版本
long lastZxidSeen; // 客户端最后见到的事务ID
int timeOut; // 会话超时时间
long sessionId; // 会话ID
buffer passwd; // 会话密码 (buffer 对应 byte[])
}
}
第二步:代码生成
在 ZooKeeper 的构建过程中,Jute 编译器会处理这个文件,并生成一个 ConnectRequest.java 文件。这个 Java 文件看起来会像这样(简化版):
package org.apache.zookeeper.proto;
import org.apache.jute.*; // 引入 Jute 核心库
public class ConnectRequest implements Record {
private int protocolVersion;
private long lastZxidSeen;
private int timeOut;
private long sessionId;
private byte[] passwd;
// ... 构造函数, getters, setters ...
// 核心:序列化方法
public void serialize(OutputArchive archive, String tag) throws java.io.IOException {
archive.startRecord(this, tag);
archive.writeInt(protocolVersion, "protocolVersion");
archive.writeLong(lastZxidSeen, "lastZxidSeen");
archive.writeInt(timeOut, "timeOut");
archive.writeLong(sessionId, "sessionId");
archive.writeBuffer(passwd, "passwd");
archive.endRecord(this, tag);
}
// 核心:反序列化方法
public void deserialize(InputArchive archive, String tag) throws java.io.IOException {
archive.startRecord(tag);
protocolVersion = archive.readInt("protocolVersion");
lastZxidSeen = archive.readLong("lastZxidSeen");
timeOut = archive.readInt("timeOut");
sessionId = archive.readLong("sessionId");
passwd = archive.readBuffer("passwd");
archive.endRecord(tag);
}
}
第三步:在代码中使用
开发者可以直接使用这个生成的 ConnectRequest 类,而无需关心其底层的字节码转换细节。
// 创建对象
ConnectRequest req = new ConnectRequest(0, 0, 4000, 0, new byte[16]);
// 序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryOutputArchive boa = new BinaryOutputArchive(baos);
req.serialize(boa, "connect"); // "connect" 是一个可选的 tag
byte[] bytes = baos.toByteArray(); // 得到序列化后的字节数组
// 反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
BinaryInputArchive bia = new BinaryInputArchive(bais);
ConnectRequest newReq = new ConnectRequest();
newReq.deserialize(bia, "connect");
OutputArchive 和 InputArchive 是 Jute 提供的接口,BinaryOutputArchive 和 BinaryInputArchive 是其二进制实现的具体类。
7.2.3 深入 Jute
Jute 的序列化格式非常紧凑。对于基本数据类型,它直接以 Big-Endian(大端字节序) 的二进制形式存储,没有额外的元数据。
int: 占用 4 字节。long: 占用 8 字节。boolean: 占用 1 字节(1 表示 true,0 表示 false)。string: 先写入一个 4 字节的int表示字符串长度,然后是 UTF-8 编码的字节内容。buffer(byte[]): 与string类似,先写入一个 4 字节的int表示字节数组长度,然后是字节内容。如果数组为 null,则长度记为 -1。vector(List): 先写入一个 4 字节的int表示列表大小,然后依次序列化列表中的每个元素。
这种格式简单、高效,且不依赖任何特定语言的特性,使得跨语言解析成为可能。
7.2.4 通信协议 (Communication Protocol)
ZooKeeper 的客户端与服务器之间的通信协议是基于 TCP 的一个异步、请求-响应式协议。
协议结构:
客户端向服务器发送的每个请求(Request)和服务器返回的每个响应(Response)在网络传输时,都由两部分组成:
- 长度头 (Length Header):一个 4 字节的
int,表示紧随其后的请求/响应体的字节长度。 - 请求/响应体 (Request/Response Body):使用 Jute 序列化后的数据。
+----------------------+----------------------------------+
| 4 字节长度 (length) | Jute 序列化的数据 (body) |
+----------------------+----------------------------------+
为什么需要长度头?
TCP 是一个流式协议,它不保留消息的边界。发送方连续发送两个包,接收方可能一次性收到一个半,也可能分两次收到。长度头的存在使得接收方可以准确地从 TCP 流中分割出一个个完整的逻辑消息。接收方首先读取 4 个字节,解析出 body 的长度 L,然后再准确地读取 L 个字节作为一条完整的消息进行 Jute 反序列化。
请求 (Request) 的结构:
请求体通常包含两部分:
- 请求头 (Request Header):包含了本次请求的元信息。
xid: 请求的唯一标识符。这是一个由客户端生成的递增序列号。服务器在响应时会原样带回这个xid,客户端可以用它来将异步收到的响应与之前发出的请求对应起来。type: 操作类型,是一个int常量,例如OpCode.CREATE(1),OpCode.DELETE(2) 等。
- 请求体 (Request Body):具体的操作数据,例如
CreateRequest对象序列化后的内容。
响应 (Response) 的结构:
响应体也包含两部分:
- 响应头 (Reply Header):
xid: 对应请求的xid。zxid: 本次操作在 ZooKeeper 服务器上被分配的事务 ID。对于所有会产生状态变更的操作,zxid都是一个全局唯一且单调递增的 ID。对于只读操作,zxid是服务器当前最新的zxid。err: 错误码。0 表示成功,其他值表示各种错误。
- 响应体 (Reply Body):操作返回的数据,例如
CreateResponse(包含新创建的节点路径)或GetDataResponse(包含节点数据和 Stat 信息)。
总结:
Jute 和 ZooKeeper 的通信协议共同构建了一个高效、可靠的底层通信框架。
- Jute 通过代码生成实现了高性能、跨语言的序列化,其紧凑的二进制格式节省了网络带宽。
- 通信协议通过
长度头 + Jute Body的结构解决了 TCP 的粘包/半包问题,通过xid实现了异步请求与响应的匹配,通过zxid和err提供了事务一致性和操作结果的反馈。
7.3 客户端 (Client)
ZooKeeper 的客户端是用户与 ZooKeeper 集群交互的入口。它不仅仅是一个简单的 API 封装,其内部包含了复杂的会话管理、网络通信和事件处理逻辑。一个稳定、高效的客户端是保证整个分布式应用可靠运行的基础。
ZooKeeper Java 客户端的核心组件主要包括:
ZooKeeper: 面向用户的顶层 API 入口。ClientCnxn: 客户端网络连接管理器,负责底层的网络 I/O。SendThread: I/O 发送线程,负责将请求序列化后发送给服务器。EventThread: 事件处理线程,负责处理服务器发来的 Watcher 事件和状态变更通知。
7.3.1 一次会话的创建过程 (Session Creation)
会话(Session)是 ZooKeeper 中一个至关重要的概念。客户端与服务器之间的所有交互都发生在会话的生命周期内。会话的创建过程,本质上就是客户端与服务器集群建立一个稳定 TCP 连接并完成注册的过程。
这个过程可以分解为以下几个核心步骤:
-
初始化
ZooKeeper对象 (API层面)- 用户代码通过
new ZooKeeper(connectString, sessionTimeout, watcher)来创建一个客户端实例。 connectString: 服务器地址列表,如"host1:2181,host2:2181,host3:2181"。sessionTimeout: 会话超时时间(毫秒)。watcher: 一个默认的 Watcher,用于接收全局的连接状态变更事件(如连接成功、断开、会话过期等)。- 在这一步,构造函数会立即创建一个
ClientCnxn对象,并启动SendThread和EventThread这两个核心后台线程。此时,真正的网络连接尚未建立。
- 用户代码通过
-
解析与选择服务器 (ClientCnxn 层面)
ClientCnxn接收到connectString后,会将其解析成一个服务器地址列表。- 如果
chroot路径被指定(例如"host:2181/my-app"),它也会被解析并保存下来。 - 客户端会打乱这个地址列表的顺序。这样做是为了避免所有客户端在启动时都尝试连接列表中的第一个服务器,从而导致“惊群效应”(Thundering Herd)。
-
尝试建立 TCP 连接 (SendThread 层面)
SendThread线程启动后,会从被打乱的地址列表中选择一个地址,尝试建立 TCP 连接。- 它使用 Java 的
Selector(NIO) 来进行非阻塞的网络连接。 - 如果连接失败(例如,服务器未启动或网络不通),
SendThread会从列表中选择下一个地址继续尝试,直到连接成功或所有地址都尝试失败。
-
发送连接请求 (SendThread 层面)
- TCP 连接建立成功后,客户端并不是马上就可用。它必须先向服务器“注册”自己,以建立一个会话。
SendThread会构建一个ConnectRequest包(使用 Jute 序列化)。这个包里包含了协议版本、客户端最后一次见到的zxid(新会话为0)、期望的sessionTimeout、会话 ID(新会话为0)和会话密码(新会话为空)等信息。- 这个请求包被发送给服务器。
-
服务器处理并返回响应
- ZooKeeper 服务器接收到
ConnectRequest后,会为这个客户端创建一个新的会话。 - 它会生成一个全局唯一的
sessionID,并确定一个最终的sessionTimeout(可能与客户端请求的不同,取决于服务器的配置限制)。 - 服务器将包含
sessionID、sessionTimeout和会话密码的ConnectResponse发送回客户端。
- ZooKeeper 服务器接收到
-
会话建立成功 (ClientCnxn, SendThread, EventThread 协同)
SendThread的Selector监听到网络可读,读取并反序列化ConnectResponse。- 它验证响应的合法性,并从中提取出
sessionID和sessionTimeout,更新ClientCnxn内部的会话状态。 - 此时,客户端的状态从
CONNECTING变为CONNECTED。 SendThread会通知EventThread。EventThread随即向用户在构造函数中注册的默认 Watcher 推送一个SyncConnected状态事件。- 至此,会话才算真正建立完成。客户端可以开始发送正常的业务请求(如
create,getData等)。
会话重连 (Session Re-establishment):
如果客户端与服务器的连接断开,但会话尚未超时,客户端会自动重复步骤 2-6,尝试连接到地址列表中的其他服务器。在重新发送 ConnectRequest 时,它会带上之前已经获取到的 sessionID 和密码,服务器会识别出这是一个已存在的会话,并允许其“恢复”。
7.3.2 服务器地址列表 (Server Address List)
服务器地址列表的管理是客户端实现高可用的关键。
- 格式: 一个由逗号分隔的
host:port列表。 - 动态更新:
ZooKeeper客户端支持在运行时动态更新服务器列表。你可以调用updateServerList(newConnectionString)方法。这在集群扩缩容或服务器地址变更时非常有用,可以避免重启客户端。 - Chroot 环境隔离: ZooKeeper 允许在一个
connectString的末尾指定一个chroot路径,例如"host:2181,host:2182/my-app"。一旦设置,该客户端之后的所有操作(如create("/node1"))都会被限制在这个/my-app目录下。实际在服务器上创建的节点路径将是/my-app/node1。这为多租户或应用隔离提供了一种便捷的命名空间管理方式。
7.3.3 ClientCnxn:网络 I/O
ClientCnxn (Client Connection) 是 ZooKeeper 客户端的“心脏”,它封装了所有底层的网络通信和协议处理细节,是 SendThread 和 EventThread 的协调者。
核心职责:
- 连接管理: 维护与 ZooKeeper 服务器的 TCP 连接,处理连接、断开和重连逻辑。
- 请求打包与排队:
- 所有来自上层 API (
ZooKeeper类) 的请求(如create,getData)并不会被立即发送。 - 它们首先被封装成
Packet对象,然后放入两个队列中:outgoingQueue: 一个待发送队列,SendThread会从这里取出请求进行序列化和发送。pendingQueue: 一个已发送但尚未收到响应的队列。请求在被SendThread发送后,会从outgoingQueue移到pendingQueue。
- 所有来自上层 API (
- 响应处理:
SendThread负责从网络中读取响应数据。- 读取到完整的响应后,它会根据响应头中的
xid去pendingQueue中找到对应的请求Packet。 - 它将响应数据填充到
Packet对象中,然后唤醒阻塞在Packet对象上的业务线程(对于同步调用)。
- 事件分发:
- 如果收到的不是业务响应,而是一个 Watcher 事件通知,
SendThread会将这个事件封装成WatcherSetEventPair对象,并放入EventThread的待处理队列waitingEvents中。
- 如果收到的不是业务响应,而是一个 Watcher 事件通知,
- 心跳维持:
SendThread在空闲时(即outgoingQueue为空)会定时向服务器发送心跳包 (Ping Request)。- 心跳的作用有两个:
- 让服务器知道客户端会话仍然存活。
- 检测客户端与服务器之间的连接是否通畅。如果在规定时间内没有收到心跳的响应,客户端会认为连接已断开,并触发重连。
SendThread 和 EventThread 的分工:
SendThread: 主攻网络 I/O。它是一个高度繁忙的线程,使用Selector(NIO) 同时处理连接、读、写事件。它负责序列化请求、发送数据、接收数据、反序列化响应,并将响应与请求匹配。为了效率,它直接将业务响应分发给等待的业务线程,将 Watcher 事件交给EventThread。EventThread: 主攻事件回调。它是一个独立的线程,专门负责处理 Watcher 事件。它会从waitingEvents队列中取出事件,并依次调用用户注册的 Watcher 回调方法(process方法)。将事件处理与网络 I/O 分离,可以避免复杂或耗时的用户回调逻辑阻塞核心的网络通信,这是非常重要的设计原则。
这个“双线程 + NIO”的设计模式,使得 ZooKeeper 客户端能够高效地处理大量的并发请求和异步事件,同时保持了对外的 API 接口简洁易用。

浙公网安备 33010602011771号