本章定位:ZNode 是 ZooKeeper 数据模型的基本单元。理解 ZNode 的类型、属性和操作语义,是使用 ZooKeeper 的起点。
定义与作用
ZooKeeper 的数据模型类似一个分布式文件系统,采用树形层次命名空间。树中的每个节点称为 ZNode。与文件系统的关键区别在于:ZNode 既可以像目录一样拥有子节点,又可以像文件一样存储数据。
ZNode 解决的核心痛点:
| 痛点 | ZNode 解决方案 |
|---|---|
| 配置数据分散 | 统一存储在命名空间的路径上,所有客户端看到一致视图 |
| 临时节点管理 | Ephemeral 节点随会话自动删除,天然表达"存活"语义 |
| 全局唯一 ID | Sequence 节点自动追加单调递增编号 |
| 数据版本控制 | Stat 中的 version 字段实现乐观锁 |
核心原理
树形命名空间
图中
/app/config存储持久配置数据,/app/services/下的子节点为 Ephemeral 类型(服务实例存活标识),/app/locks/下为带序号的临时节点。
ZNode 的核心属性
每个 ZNode 维护一个 Stat 结构,包含以下字段:
| 字段 | 含义 | 场景 |
|---|---|---|
| czxid | 创建该节点的事务 ID | 追踪创建时序 |
| mzxid | 最后一次修改该节点数据的事务 ID | 判断数据是否有更新 |
| ctime | 创建时间(毫秒级时间戳) | 生命周期追踪 |
| mtime | 最后修改时间 | 数据新鲜度判断 |
| version | 数据版本号(每次 setData +1) | 乐观锁 CAS 校验 |
| cversion | 子节点列表版本号(子节点增删 +1) | 监听子节点变化 |
| aversion | ACL 版本号 | ACL 变更追踪 |
| ephemeralOwner | 临时节点的持有者 Session ID | 0 表示持久节点 |
| dataLength | 数据长度(字节) | 数据大小限制校验 |
| numChildren | 直接子节点数量 | 目录类节点容量 |
完整示例
示例一:基本 CRUD 操作
场景说明:模拟配置中心场景,创建、读取、更新和删除配置节点。
操作前状态:空 ZooKeeper 树。
zkCli.sh -server 127.0.0.1:2181
创建节点:
[zkshell: 0] create /app "application-root"
Created /app
[zkshell: 1] create /app/config "port=8080,debug=false"
Created /app/config
读取数据:
[zkshell: 2] get /app/config
port=8080,debug=false
cZxid = 0x2
ctime = Sat Jun 13 10:00:00 CST 2026
mZxid = 0x2
mtime = Sat Jun 13 10:00:00 CST 2026
pZxid = 0x2
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 24
numChildren = 0
更新数据:
[zkshell: 3] set /app/config "port=9090,debug=true"
cZxid = 0x2
...
mZxid = 0x3 # 事务 ID 递增
dataVersion = 1 # 版本号从 0 → 1
[zkshell: 4] get /app/config
port=9090,debug=true
操作后状态:
| ZNode | dataVersion | 数据 |
|---|---|---|
| /app | 0 | application-root |
| /app/config | 1 | port=9090,debug=true |
版本号乐观锁演示:
# 使用旧版本号更新——失败
[zkshell: 5] set -v 0 /app/config "should fail"
version No is not valid : /app/config
# KeeperErrorCode = BadVersion
版本号机制保证了并发安全:客户端读到的 version=1,如果在此期间其他客户端已更新为 version=2,则使用 version=1 的 set 操作会失败。
示例二:ZNode 的 Stat 追踪
场景说明:通过 Java API 观察 ZNode 创建、修改全过程的版本变化。
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
public class ZNodeStatDemo {
public static void main(String[] args) throws Exception {
ZooKeeper zk = new ZooKeeper("127.0.0.1:2181", 3000, event -> {});
// 1. 创建节点
String path = zk.create("/stat-demo", "v1".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("Created: " + path);
// 2. 获取 Stat
Stat stat = new Stat();
byte[] data = zk.getData("/stat-demo", false, stat);
System.out.printf("version=%d, data=%s%n",
stat.getVersion(), new String(data));
// 3. 更新数据
stat = zk.setData("/stat-demo", "v2".getBytes(), stat.getVersion());
System.out.printf("After setData: version=%d%n", stat.getVersion());
// 4. 创建子节点
zk.create("/stat-demo/child", "child-data".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// 5. 再次获取 Stat
stat = zk.exists("/stat-demo", false);
System.out.printf("cversion=%d, numChildren=%d%n",
stat.getCversion(), stat.getNumChildren());
zk.close();
}
}
执行结果:
Created: /stat-demo
version=0, data=v1
After setData: version=1
cversion=1, numChildren=1
操作前后对比:
| 时间点 | version | cversion | 说明 |
|---|---|---|---|
| create 后 | 0 | 0 | 首次创建 |
| setData 后 | 1 | 0 | 只修改了数据 |
| 创建子节点后 | 1 | 1 | 只修改了子节点列表 |
示例三:数据大小限制
场景说明:验证 ZNode 数据量上限(默认 1MB)。
# 尝试写入超过 1MB 的数据
[zkshell: 0] create /large_node "(此处为超过 1MB 的字符串)"
# KeeperErrorCode = MarshallingError
操作前状态:未超出限制的节点正常创建。
操作结果:超出 1MB 限制时,服务端直接拒绝,抛出 MarshallingError。
ZooKeeper 不是通用数据库,ZNode 设计目标存储的是协调元数据(配置、状态等),通常在 KB 级别。大数据应存储在 HDFS 等系统,ZooKeeper 中只存储指针。
易错场景与面试考点
易错场景
1. 空节点不等于不存在的节点
create /empty ""
# 创建成功,dataLength=0
get /empty
# 返回空字符串,但 Stat 有效
stat /not-exist
# KeeperErrorCode = NoNode
dataLength=0 和 NoNode 是两种完全不同的状态。前者是存在的空数据节点,后者是不存在的路径。
2. version 不是全局自增 ID
容易将 version 误解为"第几次修改"。实际上 version 表示该节点自身被修改的次数,不同节点的 version 值互相独立。
3. ZNode 路径约束
以下字符不可用于路径:
\u0000(空字符)- 控制字符
\u0001~\u001F、\u007F~\u009F - Unicode 代理区
\ud800~\uF8FF、\uFFF0~\uFFFF .和..不能作为独立路径段zookeeper是保留关键字
面试高频题
Q:ZooKeeper 的数据模型与文件系统有何异同?
A:
- 相同点:树形结构、层次命名空间、路径访问(
/a/b/c) - 不同点:ZNode 可同时拥有数据和子节点(文件系统只能二选一);ZNode 支持 Ephemeral/Sequential 等特殊类型;数据大小限制在 1MB 以内;不支持相对路径。
Q:version 字段有什么用?
A:version 是实现乐观锁的核心。每次 setData 前传入期望的 version,服务端校验一致才写入。这保证了在分布式环境下无需额外加锁即可实现原子 CAS 操作。
Q:ephemeralOwner 什么时候为 0?
A:持久节点(Persistent)的 ephemeralOwner 始终为 0。只有临时节点(Ephemeral)的 ephemeralOwner 才是创建它的 Session ID。
小结
| 要点 | 说明 |
|---|---|
| ZNode 是树形结构的节点 | 兼具数据存储与目录组织能力 |
| Stat 结构 | 版本号、时间戳、事务 ID、子节点数的完整元数据 |
| version 乐观锁 | 通过 CAS 保证并发安全更新 |
| 数据上限 1MB | 适合协调元数据,不适合大对象存储 |
| 路径约束 | 禁止特定 Unicode 字符,不支持相对路径 |
ZNode 是后续所有机制的基础。下一节将深入不同类型的 ZNode(持久/临时/顺序)及其适用场景。