Zookeeper 是如何保证创建的节点是唯一的?
结论先行:唯一性来自三层保证。第一层是命名空间:znode 的完整路径在整棵树里只能存在一个,重复创建会抛
NodeExistsException。第二层是顺序号:顺序节点由服务端在父节点的cversion上自增生成 10 位序号,创建动作在 Leader 上串行执行,不会产生两个相同序号。第三层是事务:每次创建都带唯一的 zxid,全集群按同一顺序应用。
一、三层保证
| 层次 | 机制 | 保证什么 |
|---|---|---|
| 路径唯一 | 树形命名空间 + 创建时存在性检查 | 同一路径不会被创建两次 |
| 顺序唯一 | 父节点 cversion 自增,补零到 10 位 | 顺序节点的名字全局不重复 |
| 事务唯一 | 单一 Leader 串行提案 + zxid 单调递增 | 所有节点按同一顺序看到同一结果 |
| 会话唯一 | sessionId 由 Leader 分配,全局唯一 | 临时节点的所有者不混淆 |
二、并发创建同一路径时发生什么
客户端 A Leader 客户端 B
create /lock/lock-1 ────────> 检查存在?否 ──> 提案 zxid=0x1f
create /lock/lock-1
<────────────────── 检查存在?否 ──> 提案 zxid=0x20
zxid=0x1f 提交成功,节点创建
zxid=0x20 提交时发现路径已存在 → 返回 NodeExistsException
写请求全部由 Leader 串行处理,所以“检查 + 创建”是原子的,不存在两个客户端同时看到“不存在”然后都创建成功的窗口。这一点与基于数据库 select 再 insert 的实现不同,后者需要唯一索引兜底。
三、顺序号是怎么生成的
create -s /queue/item
│
▼
父节点 /queue 的 cversion = 41
│
▼
新节点名 = item + 补零(cversion, 10) = item0000000041
同时 cversion 递增到 42(在同一次事务里)
# 连续创建,观察序号
create -s /queue/item "a" # /queue/item0000000000
create -s /queue/item "b" # /queue/item0000000001
序号只保证单调递增,不保证连续:创建失败、会话断开重试都会消耗 cversion。因此不能用顺序号做“无空洞的流水号”。
四、用唯一性做互斥
try {
client.create()
.withMode(CreateMode.EPHEMERAL)
.forPath("/lock/order-1", "owner-1".getBytes());
// 创建成功即抢到锁
} catch (KeeperException.NodeExistsException e) {
// 已被别人持有,转为 watch 或换顺序节点方案
}
// 顺序节点方案:不争抢同一路径,各自创建唯一名字
String path = client.create()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath("/lock/order-1-"); // 返回 /lock/order-1-0000000007
五、版本号提供的是另一种“唯一”
setData 带 version 时,版本不匹配会抛 BadVersionException。这不是节点名唯一,而是“更新次序唯一”:
client.setData().withVersion(3).forPath("/config/db", data);
// 若期间别人改过,version 已变为 4,本次写入失败
常见追问
- 追问:临时节点会不会重名?不会,节点名唯一性与是否临时无关;临时性只影响生命周期。
- 追问:顺序号超过 10 位怎么办?序号是 32 位有符号整数,补零只是格式化,溢出后 ZK 会报错,实际业务很难达到。
- 追问:为什么不能用
create的返回值判断“第一次创建”?create成功即代表本次创建生效;要判断“是否首次初始化配置”,可用节点是否存在 + 版本号做 CAS。
小结
- 路径唯一靠命名空间与串行写,重复创建直接抛异常。
- 顺序号由父节点
cversion在事务内自增,名字唯一但可能不连续。 - zxid 保证全集群顺序一致,会话 ID 保证临时节点归属唯一。
- 版本号提供“更新唯一性”,是乐观锁的实现基础。