Zookeeper 是如何保证创建的节点是唯一的?

#中间件#面试八股

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 保证临时节点归属唯一。
  • 版本号提供“更新唯一性”,是乐观锁的实现基础。
笔记加载中…