1.2 数据模型:ZNode 与案卷目录树 本节摘要:ZooKeeper 的数据是一棵以斜杠开头的树,树上的节点叫 ZNode。ZNode 分持久、临时、持久顺序、临时顺序四种,外加 stat 元数据与版本号。理解四类节点的生命周期差异,是后面玩转锁、选主、注册中心的前提。 从一条路径说起 任何一份共享状态,第一步都要回答"放在哪、怎么找"。ZooKeeper 的答案朴素得像文件系统:一棵树,从根目录出发,斜杠分隔层级。 读起来就像 Unix 的路径——事实上你完全可以用"目录 + 文件"的直觉去理解它。差别在两处:路径上的每个节点既是"目录"也可以是"文件"(ZNode 既能有子节点,自己也能挂数据);以及每个 ZNode 的数据量被有意限制得很小。 为什么限制得这么狠?回忆 1.
本节摘要:ZooKeeper 的数据是一棵以斜杠开头的树,树上的节点叫 ZNode。ZNode 分持久、临时、持久顺序、临时顺序四种,外加 stat 元数据与版本号。理解四类节点的生命周期差异,是后面玩转锁、选主、注册中心的前提。
任何一份共享状态,第一步都要回答"放在哪、怎么找"。ZooKeeper 的答案朴素得像文件系统:一棵树,从根目录出发,斜杠分隔层级。/services/pay/order-01 读起来就像 Unix 的路径——事实上你完全可以用"目录 + 文件"的直觉去理解它。差别在两处:路径上的每个节点既是"目录"也可以是"文件"(ZNode 既能有子节点,自己也能挂数据);以及每个 ZNode 的数据量被有意限制得很小。
为什么限制得这么狠?回忆 1.1 节的定位:这份树要在集群多台机器之间保持强一致,每次写入都要走多数派表决。数据一大,表决与同步的成本按比例上升,而协调场景根本用不上大数据。把它想成书记员的案卷柜——柜子里放传票、任命书、登记表,谁也不会往里塞整箱账本。
节点类型是本节的核心考点,四种类型按"是否随会话消失"和"是否自动编号"两个维度划分。持久节点一经创建便常驻,除非有人显式删除,适合放配置这类需要长期存在的数据。临时节点的命门在"会话":创建它的客户端一旦断连(会话失效),节点被服务端自动删除——这个特性是服务注册、故障感知的地基,第四章 4.4 节会细讲会话机制。持久顺序节点和临时顺序节点则在此基础上加了自动编号:你在创建时只给前缀,服务端追加一个单调递增的十位后缀,保证同目录下先来后到一目了然。
| 类型 | 会话断开后 | 典型用途 |
|---|---|---|
| 持久节点 | 依然存在 | 配置、元信息 |
| 临时节点 | 自动删除 | 服务注册、存活标记 |
| 持久顺序节点 | 依然存在 | 分布式队列、任务编号 |
| 临时顺序节点 | 自动删除 | 分布式锁、选主排队 |
选型规律只有一句:状态应当随人走就用临时,排队需要先后就用顺序,两者都要就叠加。 记不住表格时,想想用途本身要不要"人走茶凉"。

命令行十行代码,比背表格有效得多。以下会话演示四种类型的创建与验证:
$ zkCli.sh -server 127.0.0.1:2181 # 1. 持久节点:不加任何参数就是持久 [zk] create /config "v1" Created /config # 2. 临时节点:加 -e,会话断开即删 [zk] create -e /live "i-am-here" Created /live # 3. 顺序节点:加 -s,服务端自动追加十位编号 [zk] create -s /task/task- "job" Created /task/task-0000000001 [zk] create -s /task/task- "job" Created /task/task-0000000002 # 4. 临时 + 顺序叠加:锁和选主的主力类型 [zk] create -e -s /lock/lock- "cand" Created /lock/lock-0000000003 # 断开会话重连后验证:临时节点消失,持久与顺序节点仍在 [zk] quit $ zkCli.sh -server 127.0.0.1:2181 [zk] ls /live Node does not exist: /live # 临时节点已被服务端回收 [zk] ls /task [task-0000000001, task-0000000002] # 顺序编号不复用,历史可追溯
注意最后一行:顺序节点的编号来自全局单调递增计数器,删了节点编号也不会回收。这保证了"后来者编号一定更大",分布式锁正是靠这个不变量判定先后。
每个 ZNode 除了数据,还挂着一组元数据(stat)。最重要的是 dataVersion:每次 set 都会加一。它给了客户端做条件更新的能力——"只有版本号还是 3 时才允许写入",等价于一次乐观锁。多个人同时改配置时,靠它避免后写覆盖先写。
// 乐观锁式的条件更新:先查出当前版本,再带版本提交 Stat stat = new Stat(); byte[] data = client.getData().storingStatIn(stat).forPath("/config"); System.out.println("当前版本 " + stat.getVersion()); // 例如输出 3 try { // 带版本提交:若期间别人改过(版本已不是 3),抛 BadVersion 异常 client.setData().withVersion(stat.getVersion()) .forPath("/config", "v2".getBytes()); System.out.println("更新成功"); } catch (BadVersionException e) { System.out.println("配置已被他人修改,需要重新读取再试"); }
这段代码在生产配置中心里非常实用:它把"并发冲突"从静默的数据覆盖变成了显式的异常,逼着调用方做重读与合并。4.3 节讲 Watcher 时你会看到,条件更新常与监听搭配成完整的"读-听-改"循环。
问:ZNode 能当小数据库存几百 KB 的 JSON 吗? 技术上可行,工程上别做。写路径走全集群表决,大节点会显著拉高写延迟;更稳妥的做法是 ZNode 里只存一个存储地址或配置版本号,正文放在真正的存储服务里。
下一节盘点这座法庭对外承诺的性质清单,以及它在 CAP 三角上做的那笔关键交易。
问:临时节点自己也能存数据吗,上限和持久节点一样吗? 一样,类型只决定生命周期,不改变 1 MB 的默认上限。但临时节点通常存得很小——它承载的是"谁活着、谁排第几"这类瞬态信号,大数据放进去既无意义也拖慢清算。
问:一次会话最多能创建多少个临时节点? 服务端没有单会话的硬性上限,上限来自整机的内存与配置。但大量临时节点会拖慢会话清算(要逐个删除并触发事件),单个会话名下成千上万个临时节点就是 6.2 节羊群效应的温床,应当拆分或改用持久结构。
还有一处容易忽略的细节:顺序节点的编号后缀是父节点级的计数器,同一父目录下所有顺序创建共享一个序列。这意味着不同业务若共用一个目录排号,编号会互相穿插——锁与队列必须各有各的专用目录,这是 5.2 节实现的前提。