Zookeeper 是如何保证创建的节点是唯一的?
面试考察点
- 写模型理解:面试官不仅仅是想知道你听过 "Leader 处理写请求",更是想看你能否把 "所有写请求经过 Leader → ZXID 全局排序 → 过半提交" 这条链路讲清楚。
- 并发冲突处理:多个客户端同时创建同名节点,ZooKeeper 怎么保证不会出现两个?是靠锁?靠 CAS?还是靠别的方式?
- 顺序节点原理:顺序节点的递增序号是怎么生成的?为什么不会重复?这个细节很多人说不清。
核心答案
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 开始、严格递增的。即使中间有节点被删除(比如临时节点因会话失效被清理),序号也不会回退或重复,只会继续往前走。
面试高频追问
-
如果 Leader 宕机了,正在处理的事务怎么办?
分两种情况:
- 事务已经提交(过半 ACK):新 Leader 会保留这个事务,客户端无感知。
- 事务还未提交(未过半 ACK):新 Leader 会丢弃这个事务,客户端会收到连接异常或超时。客户端需要重试。
-
Follower 能处理写请求吗?
Follower 可以接收客户端的写请求,但必须转发给 Leader 处理。Follower 自己不做任何写操作的决策。读取请求可以由 Follower 直接处理(这是 ZooKeeper 高性能读的原因)。
-
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 机制,这道题就稳了。
