Zookeeper 的数据结构是怎样的?
结论先行:ZK 的数据模型是一棵内存中的树,每个节点叫 znode,路径唯一(如
/app/config/db),节点上可挂少量数据(默认上限 1MB)和一份 stat 元信息。节点分持久、临时、顺序、容器、TTL 五类,版本号(version/cversion)支撑 CAS 式更新。
一、树形结构
/
├── app
│ ├── config
│ │ ├── db data="jdbc:mysql://..." version=3
│ │ └── cache data="redis://..." version=1
│ └── locks
│ ├── lock-0000000001 (临时顺序)
│ └── lock-0000000002
└── zookeeper
└── config (系统保留)
注意:znode 不是目录,它既能像目录一样有子节点,也能自己存数据,这与文件系统不同。
二、五种节点类型
| 类型 | 创建方式 | 生命周期 | 典型用途 |
|---|---|---|---|
| 持久节点 | create | 手动删除才消失 | 配置、元数据 |
| 临时节点 | create -e | 会话结束自动删除 | 存活标识、注册中心 |
| 持久顺序节点 | create -s | 手动删除,名字自动加序号 | 队列、锁排队 |
| 临时顺序节点 | create -e -s | 会话结束删除,名字带序号 | 分布式锁、选主 |
| 容器节点 | create -c | 子节点为空时由服务端清理 | 批量任务分组 |
临时节点不能有子节点,这是硬约束,也是“临时目录”方案行不通的原因。
三、stat 里有什么
| 字段 | 含义 |
|---|---|
czxid | 创建该节点的事务 ID |
mzxid | 最后一次修改数据的事务 ID |
pzxid | 最后一次修改子节点列表的事务 ID |
ctime / mtime | 创建 / 修改时间 |
version | 数据版本,每次 setData 加一 |
cversion | 子节点版本,子节点增删时加一,顺序号的来源 |
aversion | ACL 版本 |
ephemeralOwner | 临时节点所属会话 ID,持久节点为 0 |
dataLength / numChildren | 数据长度与子节点数 |
四、CAS 式更新
// version = -1 表示不校验;指定版本则版本不符时抛 BadVersionException
Stat stat = new Stat();
byte[] old = client.getData().storingStatIn(stat).forPath("/app/config/db");
client.setData()
.withVersion(stat.getVersion())
.forPath("/app/config/db", "jdbc:mysql://new".getBytes());
这等价于乐观锁:两个客户端同时读到 version=3,只有一个能写成功,另一个必须重读重试。
# 命令行同样支持版本
set /app/config/db "v2" 3
常见追问
- 追问:为什么限制 1MB?数据要全量加载进内存并在集群间同步,越大越拖慢写与恢复;需要存大对象时应存到对象存储,ZK 里只放引用。
- 追问:顺序号会重复吗?不会。序号由父节点的 cversion 生成,同一父节点下的创建在 Leader 上串行执行。
- 追问:顺序号连续吗?不一定。创建失败、会话断开重试都会消耗序号,所以只能保证单调递增,不能当连续 ID 用。
- 追问:数据存在磁盘还是内存?全量在内存,磁盘上只存事务日志与周期性快照,所以 ZK 的堆大小决定了能承载的数据量。
小结
- 树形命名空间 + 节点数据 + stat 元信息,是 ZK 的全部数据模型。
- 五种节点类型由“是否持久”和“是否顺序”组合而来。
version提供 CAS 语义,cversion提供顺序号。- 1MB 上限与内存存储决定了它只能放元数据。