大家好,我是31岁还在一线写代码、依然热爱技术的小米。
前两天有个粉丝私信我,说他社招面试被问到一个问题:“Zookeeper 客户端注册 Watcher 是怎么实现的?”
他当场愣住了。平时我们写代码都是:
或者:
但真被问到“内部是怎么实现的”,就卡壳了。
今天,我就用一个故事,把这个流程讲透。你会看到:
- 客户端怎么把 Watcher 带出去?
- 请求是怎么打包的?
- 服务端怎么响应?
- 客户端又是怎么把 Watcher 管起来的?
- 整个注册过程是怎么闭环的?
在本篇文章,我将带你从源码思维,一步一步拆解。
Watcher 是什么?——它像“保安订阅系统”
我喜欢用一个比喻。把 ZooKeeper 想成一个大型写字楼。
- /app/config 是某个房间
- 你是租户
- 你想知道房间有没有被改造
- 你雇了一个“保安”(Watcher)
你告诉物业:“这个房间如果装修了,第一时间通知我!”
这个“订阅动作”,就是注册 Watcher。
注册 Watcher 的入口 API
我们常用的三个 API:
源码调用示例:
这里发生了什么?
第一步:调用 API,传入 Watcher
以 getData() 为例。
核心逻辑:
- 校验 path
- 构造请求对象
- 如果 watcher != null
- 封装 WatchRegistration
源码核心思想(简化版):
这一步的本质:把 Watcher 和 path 绑定
第二步:封装 WatchRegistration
这个类非常关键。
它干嘛的?负责在客户端收到服务端响应时,把 Watcher 注册到本地管理器。
简化版结构:
实现类:
核心点:
- 不是马上注册!
- 而是等服务器响应成功后才注册!
为什么?因为如果服务器返回错误,Watcher不应该生效。
第三步:封装为 Packet 发送给服务端
Zookeeper 客户端内部有个核心类:ClientCnxn。
所有请求都会被封装成:Packet,结构大概这样:
关键点来了:WatchRegistration 被塞进 Packet!
构造代码逻辑(简化):
然后进入发送队列:
然后由发送线程发给服务端。
流程图总结一下
第四步:收到服务端响应
当服务器返回结果后,客户端读取响应:
然后执行:
注意这个 rc 是:
如果是 OK,就执行 register()
第五步:注册到 ZKWatcherManager
Zookeeper 客户端有一个核心管理类:ZKWatcherManager
内部结构:
注册逻辑:
至此,Watcher 才真正“挂”在客户端上。
完整生命周期流程图
我们用故事串起来:
- 你打电话给物业(getData)
- 告诉他我要订阅这个房间
- 物业帮你写申请单(WatchRegistration)
- 打包成快递(Packet)
- 寄给总部(服务端)
- 总部确认
- 物业把你加入订阅名单(ZKWatcherManager)
这才完成注册。
一个重要细节:为什么是一次性的?
ZooKeeper 的 Watcher 是一次性的,什么意思?
当事件触发后:
触发完成,Watcher 会被移除。
原因:为了避免状态爆炸和性能问题。
如果想持续监听?你必须:
重新注册。
社招面试该怎么答?
如果面试官问:“Zookeeper 客户端注册 Watcher 流程?”
标准结构回答:
- 调用 getData/getChildren/exists 传入 Watcher
- 构造 WatchRegistration
- 封装进 Packet
- 发送到服务端
- 收到响应后根据 rc 调用 register
- 注册到 ZKWatcherManager
- 完成注册
如果你再补一句:“Watcher 注册是延迟到服务端成功响应后才完成的”
面试官会眼睛一亮。
再看一段完整示例代码
注意:事件触发后重新注册。
总结一句话
Zookeeper 客户端注册 Watcher 的核心:延迟注册 + Packet 封装 + 本地管理器统一维护
如果把它抽象成设计模式,其实就是:
- 观察者模式
- 延迟执行
- 回调注册
最后送你一张面试总结表
END
很多人只会用:
但真正懂原理的人,会知道:Watcher 并不是立刻注册,而是在服务端成功响应后才加入本地管理器。
这就是源码思维。社招拼的不是“会用”,而是“理解底层”。
如果你喜欢这种用故事讲源码的方式,点个在看,我们下篇见。