ZooKeeper:对象、生命周期与一致性模型
ZooKeeper 树模型与会话入门
版本与前置
ZooKeeper 版本边界 为 Apache ZooKeeper 3.9.x,示例依赖固定 3.9.3,JDK 17 与 Maven 3.9。选择固定坐标是为了让源码合同可阅读,不代表推荐该补丁用于今天生产。本文核对日期 2026-10-17,依据对应 3.9.3 官方 Programmer's Guide;升级时另查补丁与安全公告。读者先理解进程、TCP、版本号与并发更新。所有示例未执行,未安装依赖或连接集群。
ZooKeeper 是小型协调状态的复制服务,不是存放大文件或业务明细的数据库。路径 /notes-course 是 znode,每个节点有数据、ACL 与 Stat。数据、子节点和 ACL 的版本分别演进,不能只看路径字符串推断所有变化。客户端通常连到一个服务端,由会话维持身份与活性;客户端 TCP 重连并不一定换会话。
路径与节点
ZooKeeper 节点类型 主要先分 persistent 与 ephemeral。persistent 不因创建者断连自动删除;ephemeral 绑定会话,临时节点不能拥有子节点。SEQUENTIAL 为路径追加有序后缀,适合排序候选者,但它本身既不授予锁,也不阻止旧进程写外部数据库。示例在持久父节点下创建 EPHEMERAL_SEQUENTIAL 子节点,以展示生命周期,并明确不实现分布式锁。
路径相同的节点只有一份;create 遇 NodeExists 是与覆盖写不同的正常竞争结果。重跑示例会保留原父节点和 ACL,不能偷偷改成开放 ACL 来消除 NoAuth。
读者命令
ZooKeeper 命令速查 要在读者已有授权会话中使用。zkCli.sh 的 get -s 显示数据和 Stat,ls 显示子节点,getAcl 查看权限。Windows 分发包对应 zkCli.cmd;它们不是操作系统通用命令。本轮不运行 CLI,也不要求读者在本站启动服务。
zkCli.sh -server 127.0.0.1:2181进入 CLI 后的 get -s /notes-course 可能报告 NoNode,因为示例尚未建立父节点。这并不说明网络失败。需要写入的读者应先按自己的 ACL 策略认证,切勿用 world:anyone 全权限当默认生产配置。
入门练习与解答
题目:连接断开两秒,临时节点是否必须消失?答:不必须,会话过期才终止所有权;短暂断连可能在协商 timeout 内恢复。题目:可否在 /member 临时节点下再建 /member/health?答:不能,应把健康信息放入临时节点数据,或放在持久父节点的兄弟节点中。把文件系统直觉套在节点树上,会得出错误结构。
中英文检索词为“ZooKeeper 会话 临时节点 watch”与“ZooKeeper programmer guide ephemeral watch version”。最终一手来源为 3.9.3 Programmer's Guide 和 Recipes。本课程没有既有历史正文需要替换。
ZooKeeper 会话 CAS Watch 与隔离令牌
会话生命周期
ZooKeeper 会话过期 意味着所有权已经丢失。Disconnected 只是连接不可用,此时不能确认自己仍拥有临时节点,不应继续凭旧锁写外部资源;Expired 则需要创建新客户端会话并重新竞争。会话 timeout 是与服务端协商的值,不是本地定时器精确触发的承诺。网络分区时服务端可能已经过期旧会话,而旧进程仍未收到通知。
ZooKeeper 临时节点 随所属 session 终止清理,不与某一次 TCP 连接一一绑定。旧进程暂停后恢复,不能因为内存还保存路径就认为自己仍是持有者。只关心“节点是否删除”而忽略写资源端权限,是锁设计中最危险的空白。
版本 CAS
ZooKeeper 版本 CAS 使用 getData 得到的 Stat.version 作为 setData 的 expected version。两人同时读版本 4,先写者成功变为 5,另一人用 4 写会收到 BadVersion。此时应重新读、重新计算业务更新,不能把 version 改为 -1 无条件覆盖。版本值仅属于这个 znode,不是整个集群业务修订号;节点删除重建后,旧版本比较还需结合节点生命周期考虑。
需要跨多个 znode 原子变更时可使用 multi 搭配 check,但它不能把外部数据库更新一起纳入事务。示例只更新一个持久节点,故意保留冲突异常供读者观察,不伪装成完整分布式事务。
Watch 重注册
ZooKeeper Watch 重注册 针对本课使用的一次性 getData watch:触发后需要再次读取并安装下一次 watch。事件只是“你缓存的状态可能失效”的通知,不携带完整历史。回调与下一次读取之间可能发生多次变更,因此读者应基于新快照做幂等处理,不按回调次数累计业务事件。不存在的节点可用 exists 注册存在性 watch;不能拿 getData 的 NoNode 当已经监听成功。
连接恢复时应重新对账状态,不能假设断线期间每一项中间值都可重放。3.9 还支持持久 watch 功能,但本示例显式使用传统一次性 watch,避免混淆两种合同。回调线程不要做长时间业务处理,否则其他通知和状态转换也会被延迟。
fencing 的资源边界
ZooKeeper fencing 隔离令牌 必须由受保护资源校验。锁候选者取得单调递增 epoch 后,每次写资源都带 epoch,资源保存已接受的最大 epoch 并原子拒绝较小值。这样新持有者开始工作后,旧暂停进程恢复的陈旧请求才会被挡住。只把 sequence 打到日志或在客户端比较,不能阻止旧进程直接写数据库。
顺序节点有作用域与生命周期边界,不可在父节点重建、计数回绕等情况仍无条件宣称令牌永久单调。生产设计应明确 epoch 的持久来源、溢出与恢复策略,并在数据库条件写入或其他资源协议中实施拒绝;本课不提供未验证的完整锁实现。
练习与答案
题目:看到 NodeDataChanged 后能否直接使用回调前缓存值?答:不能,应读取新值并重装 watch。题目:CAS 冲突后重试原样 setData 是否安全?答:可能覆盖别人刚加的字段,应重新计算。题目:A 暂停、会话过期,B 获锁,A 恢复,谁挡住 A?答:有持久 epoch 检查的实际资源,而不是 Watch 通知速度。
适用 3.9.x,核对 2026-10-17;依据 Programmer's Guide 的会话、Stat、watch 合同与 锁 Recipes。
本页为理论说明,关联示例未执行;没有连接服务或验证分布式时序。
继续阅读
- 完整实践
- 问题排查