跳到主要内容

数据模型与 ZNode

ZooKeeper 的数据模型是其核心概念之一。理解数据模型和 ZNode 对于正确使用 ZooKeeper 至关重要。

层次化命名空间

ZooKeeper 使用层次化的命名空间,类似于 Unix 文件系统的目录树结构。命名空间由数据节点(称为 znode)组成,每个 znode 可以存储数据,也可以有子节点。

                    /

┌───────────┼───────────┐
│ │ │
/zookeeper /app /services
│ │ │
config users order-service
│ │
user1 instance-1

路径规范

ZooKeeper 的路径遵循以下规范:

  • 路径以斜杠 / 开头,表示绝对路径
  • 路径元素之间用斜杠分隔
  • 路径不能以斜杠结尾(根路径除外)
  • 支持 Unicode 字符

路径限制

无效字符:
- 空字符 \u0000
- 控制字符 \u0001 - \u001F, \u007F - \u009F
- 非法字符 \ud800 - \uF8FF, \uFFF0 - \uFFFF

保留字:
- zookeeper(保留路径)
- . 和 .. 不能单独作为路径元素

ZNode 详解

ZNode 是 ZooKeeper 数据模型的核心概念。每个 znode 由路径名称和数据内容组成。

ZNode 的结构

每个 znode 包含以下信息:

┌─────────────────────────────────────────────────────────────┐
│ ZNode │
├─────────────────────────────────────────────────────────────┤
│ 路径: /app/config │
├─────────────────────────────────────────────────────────────┤
│ 数据内容: "database=mysql" │
├─────────────────────────────────────────────────────────────┤
│ Stat 结构: │
│ ├── czxid: 创建时的事务 ID │
│ ├── mzxid: 最后修改的事务 ID │
│ ├── pzxid: 子节点最后修改的事务 ID │
│ ├── ctime: 创建时间 │
│ ├── mtime: 最后修改时间 │
│ ├── version: 数据版本号 │
│ ├── cversion: 子节点版本号 │
│ ├── aversion: ACL 版本号 │
│ ├── ephemeralOwner: 临时节点所有者会话 ID │
│ ├── dataLength: 数据长度 │
│ └── numChildren: 子节点数量 │
└─────────────────────────────────────────────────────────────┘

Stat 结构详解

public class Stat {
// 创建时的事务 ID(ZooKeeper Transaction ID)
public long czxid;

// 最后修改的事务 ID
public long mzxid;

// 子节点最后修改的事务 ID
public long pzxid;

// 创建时间(毫秒)
public long ctime;

// 最后修改时间(毫秒)
public long mtime;

// 数据版本号(每次更新 +1)
public int version;

// 子节点版本号(子节点变更时 +1)
public int cversion;

// ACL 版本号(ACL 变更时 +1)
public int aversion;

// 临时节点的会话 ID,持久节点为 0
public long ephemeralOwner;

// 数据长度(字节)
public int dataLength;

// 子节点数量
public int numChildren;
}

版本号的作用

版本号用于实现乐观锁机制。更新或删除节点时,可以指定版本号进行条件更新:

// 只有当版本号匹配时才更新
zk.setData("/config", newData, currentVersion);

// 版本号不匹配时抛出 BadVersionException

ZNode 类型

ZooKeeper 支持多种类型的 znode,每种类型有不同的生命周期和特性。

1. 持久节点(Persistent)

持久节点是最基本的节点类型,创建后会一直存在,直到被显式删除。

特性:
- 生命周期:永久存在,直到被删除
- 子节点:可以有子节点
- 用途:存储配置信息、静态数据

创建示例

# 创建持久节点
create /config "app-config"

# 创建持久节点(带子节点)
create /config/db "mysql"
create /config/cache "redis"

2. 临时节点(Ephemeral)

临时节点的生命周期与创建它的会话绑定,会话结束后节点自动删除。

特性:
- 生命周期:会话有效期内存在
- 子节点:不能有子节点
- 用途:服务注册、分布式锁

创建示例

# 创建临时节点
create -e /services/order-service/host1 "192.168.1.100:8080"

# 会话断开后,节点自动删除

注意事项

  • 临时节点不能有子节点
  • 会话断开后,临时节点立即删除
  • 适用于服务注册、临时状态存储

3. 持久顺序节点(Persistent Sequential)

持久顺序节点在创建时,ZooKeeper 会自动在节点名后添加一个递增的序号。

特性:
- 生命周期:永久存在
- 子节点:可以有子节点
- 序号:自动添加 10 位数字序号
- 用途:分布式队列、Leader 选举

创建示例

# 创建持久顺序节点
create -s /queue/task "task-data"
# 实际创建的节点名:/queue/task0000000001

create -s /queue/task "task-data"
# 实际创建的节点名:/queue/task0000000002

4. 临时顺序节点(Ephemeral Sequential)

临时顺序节点结合了临时节点和顺序节点的特性。

特性:
- 生命周期:会话有效期内存在
- 子节点:不能有子节点
- 序号:自动添加 10 位数字序号
- 用途:分布式锁、Leader 选举

创建示例

# 创建临时顺序节点
create -e -s /lock/resource "client-1"
# 实际创建的节点名:/lock/resource0000000001

5. 容器节点(Container)

容器节点是 ZooKeeper 3.5.x 引入的特殊节点类型,当其最后一个子节点被删除后,容器节点会被自动删除。

特性:
- 生命周期:子节点全部删除后可能被删除
- 子节点:可以有子节点
- 用途:锁、Leader 选举的父节点

创建示例

# 创建容器节点
create -c /locks "locks-container"

# 创建子节点
create -e /locks/resource-1 "lock-data"

# 当所有子节点删除后,容器节点可能被删除

6. TTL 节点(Time To Live)

TTL 节点在指定时间内未被修改且没有子节点时,会被自动删除。

特性:
- 生命周期:TTL 时间内未被修改则删除
- 子节点:不能有子节点(TTL 临时节点)
- 用途:临时数据存储、缓存

创建示例

# 创建 TTL 节点(需要启用 TTL 功能)
# 在 zoo.cfg 中添加:zookeeper.extendedTypesEnabled=true

create -t 30000 /cache/data "cached-value"
# 30 秒内未被修改且无子节点,则自动删除

节点类型对比

类型生命周期子节点序号典型用途
持久节点永久支持配置存储
临时节点会话期不支持服务注册
持久顺序节点永久支持分布式队列
临时顺序节点会话期不支持分布式锁
容器节点条件删除支持锁容器
TTL 节点定时删除不支持缓存

数据存储

数据大小限制

ZooKeeper 设计用于存储协调数据,数据大小应该相对较小:

建议数据大小:
- 推荐:小于 1KB
- 上限:1MB(硬限制)
- 实际:大多数场景下几百字节即可

为什么限制数据大小

  1. ZooKeeper 数据保存在内存中,大数据占用过多内存
  2. 网络传输大数据会增加延迟
  3. 写入大数据会影响整体性能

数据读写特性

原子性

  • 读取操作获取 znode 的所有数据
  • 写入操作替换 znode 的所有数据
  • 没有部分读取或部分写入
// 读取所有数据
byte[] data = zk.getData("/config", false, null);

// 写入替换所有数据
zk.setData("/config", newData, -1);

Zxid(事务 ID)

Zxid 是 ZooKeeper 事务的唯一标识符,是一个 64 位的数字:

┌─────────────────────────────────────────────────────────────┐
│ Zxid 结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 高 32 位(epoch) 低 32 位(计数器) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Leader 任期 │ │ 事务序号 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ 示例:0x100000001 = epoch 1, counter 1 │
│ │
└─────────────────────────────────────────────────────────────┘

Zxid 的作用

  • 全局有序:所有事务按 zxid 排序
  • 因果关系:zxid 小的事务先发生
  • 状态同步:用于 Leader 选举和数据同步

命令行操作

创建节点

# 创建持久节点
[zk: localhost:2181(CONNECTED) 0] create /app "data"
Created /app

# 创建临时节点
[zk: localhost:2181(CONNECTED) 1] create -e /temp "temp-data"
Created /temp

# 创建顺序节点
[zk: localhost:2181(CONNECTED) 2] create -s /seq "seq-data"
Created /seq0000000001

# 创建临时顺序节点
[zk: localhost:2181(CONNECTED) 3] create -e -s /eseq "eseq-data"
Created /eseq0000000002

读取节点

# 获取节点数据
[zk: localhost:2181(CONNECTED) 4] get /app
data
cZxid = 0x100000001
ctime = Wed Jan 01 10:00:00 CST 2025
mZxid = 0x100000001
mtime = Wed Jan 01 10:00:00 CST 2025
pZxid = 0x100000001
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0

# 获取节点数据(仅数据)
[zk: localhost:2181(CONNECTED) 5] get -s /app
data

# 列出子节点
[zk: localhost:2181(CONNECTED) 6] ls /
[app, zookeeper]

# 列出子节点(带状态)
[zk: localhost:2181(CONNECTED) 7] ls -s /
[app, zookeeper]
cZxid = 0x0
ctime = Thu Jan 01 00:00:00 CST 1970
...

更新节点

# 更新节点数据
[zk: localhost:2181(CONNECTED) 8] set /app "new-data"
cZxid = 0x100000001
ctime = Wed Jan 01 10:00:00 CST 2025
mZxid = 0x100000002
mtime = Wed Jan 01 10:05:00 CST 2025
...

# 条件更新(指定版本号)
[zk: localhost:2181(CONNECTED) 9] set /app "data-v2" 1

删除节点

# 删除节点(无子节点)
[zk: localhost:2181(CONNECTED) 10] delete /app

# 条件删除(指定版本号)
[zk: localhost:2181(CONNECTED) 11] delete /app 0

# 递归删除(包含子节点)
[zk: localhost:2181(CONNECTED) 12] deleteall /app

检查节点

# 检查节点是否存在
[zk: localhost:2181(CONNECTED) 13] stat /app
cZxid = 0x100000001
ctime = Wed Jan 01 10:00:00 CST 2025
...

# 获取节点状态
[zk: localhost:2181(CONNECTED) 14] stat /nonexistent
Node does not exist

Java API 操作

创建连接

import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.Watcher;

public class ZooKeeperDemo {
public static void main(String[] args) throws Exception {
Watcher watcher = event -> {
System.out.println("Event: " + event.getType());
};

ZooKeeper zk = new ZooKeeper("localhost:2181", 3000, watcher);

Thread.sleep(Long.MAX_VALUE);
}
}

创建节点

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs.Ids;

public class CreateNodeDemo {
public static void main(String[] args) throws Exception {
ZooKeeper zk = new ZooKeeper("localhost:2181", 3000, null);

String path1 = zk.create(
"/persistent",
"data".getBytes(),
Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT
);
System.out.println("Created: " + path1);

String path2 = zk.create(
"/ephemeral",
"temp".getBytes(),
Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL
);
System.out.println("Created: " + path2);

String path3 = zk.create(
"/sequential-",
"seq".getBytes(),
Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT_SEQUENTIAL
);
System.out.println("Created: " + path3);

zk.close();
}
}

读取数据

import org.apache.zookeeper.data.Stat;

public class GetDataDemo {
public static void main(String[] args) throws Exception {
ZooKeeper zk = new ZooKeeper("localhost:2181", 3000, null);

Stat stat = new Stat();
byte[] data = zk.getData("/config", false, stat);

System.out.println("Data: " + new String(data));
System.out.println("Version: " + stat.getVersion());
System.out.println("Children: " + stat.getNumChildren());

zk.close();
}
}

更新数据

public class SetDataDemo {
public static void main(String[] args) throws Exception {
ZooKeeper zk = new ZooKeeper("localhost:2181", 3000, null);

Stat stat = zk.setData("/config", "new-data".getBytes(), -1);
System.out.println("New version: " + stat.getVersion());

zk.close();
}
}

删除节点

public class DeleteNodeDemo {
public static void main(String[] args) throws Exception {
ZooKeeper zk = new ZooKeeper("localhost:2181", 3000, null);

zk.delete("/config", -1);

zk.close();
}
}

小结

本章深入学习了 ZooKeeper 的数据模型和 ZNode:

  1. 层次化命名空间:类似文件系统的树形结构
  2. ZNode 类型:持久、临时、顺序、容器、TTL 等多种类型
  3. Stat 结构:包含版本号、时间戳等元数据
  4. Zxid:事务 ID,用于保证顺序性
  5. 数据操作:创建、读取、更新、删除的基本操作

下一章我们将学习 ZooKeeper 的会话和 Watcher 机制。