数据模型与 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(硬限制)
- 实际:大多数场景下几百字节即可
为什么限制数据大小:
- ZooKeeper 数据保存在内存中,大数据占用过多内存
- 网络传输大数据会增加延迟
- 写入大数据会影响整体性能
数据读写特性
原子性:
- 读取操作获取 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:
- 层次化命名空间:类似文件系统的树形结构
- ZNode 类型:持久、临时、顺序、容器、TTL 等多种类型
- Stat 结构:包含版本号、时间戳等元数据
- Zxid:事务 ID,用于保证顺序性
- 数据操作:创建、读取、更新、删除的基本操作
下一章我们将学习 ZooKeeper 的会话和 Watcher 机制。