1.2 数据模型:ZNode 与案卷目录树


文档摘要

1.2 数据模型:ZNode 与案卷目录树 本节摘要:ZooKeeper 的数据是一棵以斜杠开头的树,树上的节点叫 ZNode。ZNode 分持久、临时、持久顺序、临时顺序四种,外加 stat 元数据与版本号。理解四类节点的生命周期差异,是后面玩转锁、选主、注册中心的前提。 从一条路径说起 任何一份共享状态,第一步都要回答"放在哪、怎么找"。ZooKeeper 的答案朴素得像文件系统:一棵树,从根目录出发,斜杠分隔层级。 读起来就像 Unix 的路径——事实上你完全可以用"目录 + 文件"的直觉去理解它。差别在两处:路径上的每个节点既是"目录"也可以是"文件"(ZNode 既能有子节点,自己也能挂数据);以及每个 ZNode 的数据量被有意限制得很小。 为什么限制得这么狠?回忆 1.

1.2 数据模型:ZNode 与案卷目录树

本节摘要:ZooKeeper 的数据是一棵以斜杠开头的树,树上的节点叫 ZNode。ZNode 分持久、临时、持久顺序、临时顺序四种,外加 stat 元数据与版本号。理解四类节点的生命周期差异,是后面玩转锁、选主、注册中心的前提。

从一条路径说起

任何一份共享状态,第一步都要回答"放在哪、怎么找"。ZooKeeper 的答案朴素得像文件系统:一棵树,从根目录出发,斜杠分隔层级。/services/pay/order-01 读起来就像 Unix 的路径——事实上你完全可以用"目录 + 文件"的直觉去理解它。差别在两处:路径上的每个节点既是"目录"也可以是"文件"(ZNode 既能有子节点,自己也能挂数据);以及每个 ZNode 的数据量被有意限制得很小。

为什么限制得这么狠?回忆 1.1 节的定位:这份树要在集群多台机器之间保持强一致,每次写入都要走多数派表决。数据一大,表决与同步的成本按比例上升,而协调场景根本用不上大数据。把它想成书记员的案卷柜——柜子里放传票、任命书、登记表,谁也不会往里塞整箱账本。

四种 ZNode:不同期限的卷宗

节点类型是本节的核心考点,四种类型按"是否随会话消失"和"是否自动编号"两个维度划分。持久节点一经创建便常驻,除非有人显式删除,适合放配置这类需要长期存在的数据。临时节点的命门在"会话":创建它的客户端一旦断连(会话失效),节点被服务端自动删除——这个特性是服务注册、故障感知的地基,第四章 4.4 节会细讲会话机制。持久顺序节点临时顺序节点则在此基础上加了自动编号:你在创建时只给前缀,服务端追加一个单调递增的十位后缀,保证同目录下先来后到一目了然。

类型 会话断开后 典型用途
持久节点 依然存在 配置、元信息
临时节点 自动删除 服务注册、存活标记
持久顺序节点 依然存在 分布式队列、任务编号
临时顺序节点 自动删除 分布式锁、选主排队

选型规律只有一句:状态应当随人走就用临时,排队需要先后就用顺序,两者都要就叠加。 记不住表格时,想想用途本身要不要"人走茶凉"。

图:案卷柜——一棵真实的 ZNode 树与节点属性标注

图:案卷柜——一棵真实的 ZNode 树与节点属性标注

动手卷一卷:四种节点各建一个

命令行十行代码,比背表格有效得多。以下会话演示四种类型的创建与验证:

$ 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] # 顺序编号不复用,历史可追溯

注意最后一行:顺序节点的编号来自全局单调递增计数器,删了节点编号也不会回收。这保证了"后来者编号一定更大",分布式锁正是靠这个不变量判定先后。

stat 与版本号:藏在节点里的台账

每个 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 时你会看到,条件更新常与监听搭配成完整的"读-听-改"循环。

要点回顾

  • 模型一句话:一棵小树,路径即地址,节点既可当目录也可存数据,单节点默认 1 MB 上限。
  • 两种维度四种类型:临时与否看会话存续,顺序与否看自动编号;锁与选主多用临时顺序节点。
  • 编号不复用:顺序后缀来自全局计数器,删除不回收,保证先后关系稳定可判。
  • 版本号即乐观锁:dataVersion 配合条件更新,把并发覆盖变成显式冲突异常。

常见疑问

问:ZNode 能当小数据库存几百 KB 的 JSON 吗? 技术上可行,工程上别做。写路径走全集群表决,大节点会显著拉高写延迟;更稳妥的做法是 ZNode 里只存一个存储地址或配置版本号,正文放在真正的存储服务里。

下一节盘点这座法庭对外承诺的性质清单,以及它在 CAP 三角上做的那笔关键交易。

延伸追问

问:临时节点自己也能存数据吗,上限和持久节点一样吗? 一样,类型只决定生命周期,不改变 1 MB 的默认上限。但临时节点通常存得很小——它承载的是"谁活着、谁排第几"这类瞬态信号,大数据放进去既无意义也拖慢清算。

问:一次会话最多能创建多少个临时节点? 服务端没有单会话的硬性上限,上限来自整机的内存与配置。但大量临时节点会拖慢会话清算(要逐个删除并触发事件),单个会话名下成千上万个临时节点就是 6.2 节羊群效应的温床,应当拆分或改用持久结构。

还有一处容易忽略的细节:顺序节点的编号后缀是父节点级的计数器,同一父目录下所有顺序创建共享一个序列。这意味着不同业务若共用一个目录排号,编号会互相穿插——锁与队列必须各有各的专用目录,这是 5.2 节实现的前提。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U