enyang
enyang
Published on 2026-01-28 / 1 Visits
0
0

ZooKeeper<2>——数据模型与节点操作

1 数据模型概述

ZooKeeper 的核心是一个分布式协调服务,其数据存储方式类似轻量级的文件系统,但又有很强的分布式语义保证。

1.1 节点(ZNode)的基本概念

  • ZNode 是唯一的数据单元,每个 ZNode 在整个树中通过唯一的路径标识。

  • 数据存储:ZNode 内部存储的是 byte[],可以存储任何序列化后的对象(JSON、Protobuf、String 等)。

  • 层级关系:节点可拥有子节点,形成树状结构,根节点 / 是所有节点的顶层。

  • 元数据 Stat:每个节点都带有元信息:

    • czxid:节点创建事务 ID

    • mzxid:最后一次修改事务 ID

    • ctime / mtime:创建与修改时间

    • version:数据版本号

    • cversion:子节点版本号

    • ephemeralOwner:临时节点所属 session id

    • dataLength / numChildren:数据长度和子节点数量

ZooKeeper 通过 versioncversion 提供了轻量级的一致性保障,使节点数据操作可安全地实现 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 节点类型选择指南

类型

生命周期

子节点

应用场景

持久

手动删除

配置中心、共享状态

临时

session 断开自动删除

不可

服务注册、临时锁

顺序

自动编号

分布式队列、锁顺序控制

顺序节点和临时节点结合,是实现分布式锁的经典方法。

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 保证更新原子性,每次更新都会生成新的 mzxidversion

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);
  • 通过顺序节点保证任务处理顺序。


Comment