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


面试考察点

  1. 写模型理解:面试官不仅仅是想知道你听过 "Leader 处理写请求",更是想看你能否把 "所有写请求经过 Leader → ZXID 全局排序 → 过半提交" 这条链路讲清楚。
  2. 并发冲突处理:多个客户端同时创建同名节点,ZooKeeper 怎么保证不会出现两个?是靠锁?靠 CAS?还是靠别的方式?
  3. 顺序节点原理:顺序节点的递增序号是怎么生成的?为什么不会重复?这个细节很多人说不清。

核心答案

ZooKeeper 通过以下三层机制保证节点唯一:

层次机制作用
第一层:Leader 集中处理所有写请求转发给 Leader,由 Leader 串行处理避免多节点并发写入冲突
第二层:ZXID 全局排序每个写操作分配全局唯一递增的事务 ID保证操作的全局顺序
第三层:过半提交事务必须被超过半数节点确认才生效保证数据一致性

一句话:所有写操作最终都在 Leader 上串行执行,自然不会产生冲突。

深度解析

一、第一层防线:Leader 集中处理写请求

这是最关键的一层,也是很多人忽略的。ZooKeeper 的写模型是 Leader-Only Write

上图展示了 ZooKeeper 处理写请求的完整流程。关键点:

  • 步骤 ①②:无论客户端连接的是 Leader 还是 Follower,所有写请求都会被转发给 Leader 处理。Follower 自己不处理写操作,只负责转发。
  • 步骤 ③:Leader 收到写请求后,在本地串行处理。注意,这里是串行的——Leader 内部只有一个线程处理事务请求(通过请求队列),所以不可能同时处理两个创建同名节点的请求。
  • 步骤 ④⑤⑥:Leader 为这个写操作生成一个事务提案,广播给所有 Follower。收到过半 Follower 的 ACK 后,提交事务。
  • 步骤 ⑦:事务提交成功后,返回给客户端。

核心结论:不管有多少个客户端并发创建同名节点,这些请求最终都会汇聚到 Leader 的同一个处理队列中,排队串行执行。第一个创建成功,后面的直接失败。

这跟数据库的乐观锁不一样,ZooKeeper 用的就是最朴素的思路——所有写操作在 Leader 上串行化,从源头杜绝并发冲突。

二、第二层防线:ZXID 全局排序

Leader 串行处理是保证了同一时刻只有一个写操作在执行,那如果 Leader 切换了呢?新 Leader 怎么知道之前的操作顺序?这就靠 ZXID 了。

ZXID(ZooKeeper Transaction ID) 是一个 64 位整数,由两部分组成:

ZXID 保证了:

  • 全局唯一:每个事务的 ZXID 都不同,不存在两个事务共享同一个 ZXID。
  • 严格递增:后发生的事务 ZXID 一定比先发生的大。Leader 每处理一个写请求,counter 就 +1。
  • 跨 Leader 保序:Leader 切换后,epoch 会 +1,counter 重置。新 Leader 的 ZXID 一定大于旧 Leader 的所有 ZXID(因为高 32 位更大了),所以全局顺序不会乱。

这个设计确实优雅。通过 epoch + counter 的组合,既保证了同一 Leader 任期内的顺序,又保证了跨任期的顺序。

三、第三层防线:过半提交(Quorum)

即使 Leader 串行处理了,如果事务还没提交 Leader 就挂了呢?过半机制保证了一致性:

  • Leader 提出事务提案后,必须收到超过半数节点的 ACK 才能提交。
  • 未提交的事务对客户端不可见。
  • 如果 Leader 在提交前挂了,新 Leader 会根据 ZXID 决定是提交还是丢弃这些未完成的事务。

四、同名节点创建失败的处理

那具体到 "创建同名节点" 这个场景,流程是这样的:

上图展示了两个客户端并发创建同名节点的处理过程。因为 Leader 是串行处理的,所以:

  • 请求 A 先到达,检查 /lock 不存在,创建成功。
  • 请求 B 后到达,检查 /lock 已存在(A 刚创建的),直接抛出 NodeExistsException

整个过程不需要加锁、不需要 CAS,就是简单的串行检查 + 创建。因为只有一个线程在处理,天然不会冲突。

五、顺序节点怎么保证唯一?

顺序节点(Sequential Node)的唯一性稍有不同。创建顺序节点时,客户端指定一个前缀(如 lock-),ZooKeeper 自动在后面追加递增数字:

顺序节点序号的生成依赖父节点的 cversion(子节点版本号),每次创建子节点时 cversion +1。因为所有写操作都在 Leader 上串行执行,所以 cversion 的递增是原子性的,不可能出现两个子节点拿到相同的序号。

而且,这个序号是从 0 开始、严格递增的。即使中间有节点被删除(比如临时节点因会话失效被清理),序号也不会回退或重复,只会继续往前走。

面试高频追问

  1. 如果 Leader 宕机了,正在处理的事务怎么办?

    分两种情况:

    • 事务已经提交(过半 ACK):新 Leader 会保留这个事务,客户端无感知。
    • 事务还未提交(未过半 ACK):新 Leader 会丢弃这个事务,客户端会收到连接异常或超时。客户端需要重试。
  2. Follower 能处理写请求吗?

    Follower 可以接收客户端的写请求,但必须转发给 Leader 处理。Follower 自己不做任何写操作的决策。读取请求可以由 Follower 直接处理(这是 ZooKeeper 高性能读的原因)。

  3. ZXID 和数据库的自增 ID 有什么区别?

    数据库的自增 ID 只在一个实例上有意义,而 ZXID 是分布式环境下的全局 ID,通过 epoch + counter 的设计保证跨节点、跨 Leader 的全局唯一和递增。而且 ZXID 还承载了 "任期" 信息,可以用来检测和拒绝旧 Leader 的请求。

常见面试变体

  • "ZooKeeper 的写请求处理流程是什么?"
  • "ZooKeeper 的 ZXID 是什么?有什么用?"
  • "多个客户端并发创建同一个节点会发生什么?"
  • "ZooKeeper 顺序节点的序号是怎么生成的?"

记忆口诀

节点唯一三件套:Leader 串行处理(源头防冲突)→ ZXID 全局排序(跨 Leader 保序)→ 过半提交(一致性兜底)。

一句话概括:所有写操作都在 Leader 上排队,一个一个来,天然不会冲突。

顺序节点:用 cversion 递增做序号,Leader 串行处理保证原子性。

总结

ZooKeeper 保证节点唯一的核心就一句话:所有写操作在 Leader 上串行处理。不管多少个客户端并发创建同名节点,请求最终都汇聚到 Leader 的同一个队列中排队执行,第一个成功、后续的抛 NodeExistsException。ZXID 提供全局事务排序,过半提交保证数据一致性。面试时把 "Leader 串行 → ZXID 排序 → 过半提交" 这三层防线讲清楚,再补充顺序节点的 cversion 机制,这道题就稳了。