1 数据模型概述
ZooKeeper 的核心是一个分布式协调服务,其数据存储方式类似轻量级的文件系统,但又有很强的分布式语义保证。
1.1 节点(ZNode)的基本概念
ZNode 是唯一的数据单元,每个 ZNode 在整个树中通过唯一的路径标识。
数据存储:ZNode 内部存储的是
byte[],可以存储任何序列化后的对象(JSON、Protobuf、String 等)。层级关系:节点可拥有子节点,形成树状结构,根节点
/是所有节点的顶层。元数据 Stat:每个节点都带有元信息:
czxid:节点创建事务 IDmzxid:最后一次修改事务 IDctime/mtime:创建与修改时间version:数据版本号cversion:子节点版本号ephemeralOwner:临时节点所属 session iddataLength/numChildren:数据长度和子节点数量
ZooKeeper 通过 version 和cversion 提供了轻量级的一致性保障,使节点数据操作可安全地实现 CAS。
1.2 树状层次结构
/
├── app
│ ├── config
│ └── workers
├── services
│ └── queue
└── locks
节点
/app/config可以存储配置数据。节点
/services/queue可以存储任务队列信息。节点
/locks可用作分布式锁根目录。
2 节点类型与生命周期
ZooKeeper 的节点类型决定了节点的生命周期和用途。
2.1 持久节点(Persistent Node)
创建后永久存在,除非显式删除。
常用于存储:
系统配置
注册服务的全局信息
示例:
zk.create("/app/config", "v1".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
2.2 临时节点(Ephemeral Node)
生命周期与 session 绑定:
session 断开 → 节点自动删除。
常用于:
服务注册
临时锁
特点:
无法创建子节点
创建失败则抛出
KeeperException
示例:
zk.create("/app/worker1", "online".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
2.3 顺序节点(Sequential Node)
创建节点时,ZooKeeper 会自动追加递增序号(10 位十进制)。
可与持久/临时节点结合:
/task-000000001(持久顺序)/lock-000000001(临时顺序)
常用于:
分布式队列
分布式锁顺序控制
String path = zk.create("/locks/lock-", "thread1".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("顺序节点创建成功: " + path);
2.4 节点类型选择指南
顺序节点和临时节点结合,是实现分布式锁的经典方法。
3 节点操作 API
ZooKeeper 提供标准 API 对节点进行增删改查(CRUD)操作。注意,每个操作都可以结合版本号实现 CAS(Compare And Set)。
3.1 创建节点(create)
String path = zk.create("/app/config", "v1".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
参数说明:
path:节点路径data:数据(byte[])acl:访问控制列表CreateMode:节点类型
返回值:节点实际路径(顺序节点会追加编号)
创建节点时父节点必须存在,否则会报 NoNodeException。可使用递归创建工具类。
3.2 删除节点(delete)
zk.delete("/app/config", -1); // -1 表示忽略版本
使用版本号可以防止误删:
zk.delete("/app/config", stat.getVersion());
3.3 更新节点数据(setData)
Stat stat = zk.setData("/app/config", "v2".getBytes(), -1);
System.out.println("更新后版本号: " + stat.getVersion());
版本控制:
版本号不匹配 → 更新失败抛
KeeperException.BadVersionException
原理:ZooKeeper 保证更新原子性,每次更新都会生成新的
mzxid和version。
3.4 获取节点数据(getData)
Stat stat = new Stat();
byte[] data = zk.getData("/app/config", false, stat);
System.out.println("节点数据: " + new String(data));
System.out.println("版本号: " + stat.getVersion());
3.5 获取子节点列表(getChildren)
List<String> children = zk.getChildren("/app", false);
for(String c : children) {
System.out.println("子节点: " + c);
}
watch参数可注册子节点变化事件。
4 Watcher 机制与事件通知
Watcher 是 ZooKeeper 的核心事件机制,用于事件驱动型通知。
4.1 Watcher 工作原理
一次性触发:
事件触发后,Watcher 自动失效,需要重新注册。
异步回调:
服务端触发 → 客户端回调
轻量高效:
不轮询,不占用大量资源
4.2 节点数据变化通知
zk.getData("/app/config", event -> {
System.out.println("节点数据变化: " + event.getType());
}, null);
EventType.NodeDataChanged表示节点数据更新EventType.NodeDeleted表示节点删除
4.3 子节点变化通知
zk.getChildren("/app", event -> {
System.out.println("子节点变化: " + event.getType());
}, null);
事件类型:
NodeChildrenChanged→ 子节点增加或删除NodeDeleted→ 父节点被删除
4.4 Watcher 高级注意事项
Watcher 不能保证顺序性:不同客户端接收到事件的顺序可能不同。
事件聚合:同一节点在短时间内多次变化,只触发一次 Watcher。
一次性限制:需要再次注册 Watcher 才能监听下一次变化。
5 实战示例
5.1 创建节点并监听数据变化
// 创建节点
zk.create("/services/app1", "running".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// 监听数据变化
zk.getData("/services/app1", event -> {
System.out.println("节点状态变化: " + event.getType());
}, null);
// 模拟更新
zk.setData("/services/app1", "stopped".getBytes(), -1);
5.2 分布式锁示例(临时顺序节点)
// 创建临时顺序节点
String lockPath = zk.create("/locks/lock-", "thread1".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取锁列表
List<String> locks = zk.getChildren("/locks", false);
Collections.sort(locks);
if (lockPath.endsWith(locks.get(0))) {
System.out.println("获取锁成功");
} else {
System.out.println("等待锁释放");
}
最小序号节点获得锁,其他节点监听前一个节点删除事件,实现公平锁。
5.3 分布式任务队列示例
// 添加任务
String taskPath = zk.create("/services/app1/task-", "task1".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
// 获取任务列表
List<String> tasks = zk.getChildren("/services/app1", false);
Collections.sort(tasks);
System.out.println("当前任务队列: " + tasks);
通过顺序节点保证任务处理顺序。