乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • ZooKeeper 学习路径
  • 第1章 分布式协调与ZooKeeper概述

    • ZooKeeper 概述
  • 第2章 单机与集群搭建

    • 配置参数详解
    • 集群搭建
  • 第3章 数据模型与ZNode

    • ZNode 详解
    • 节点类型对比
    • 顺序节点
    • ACL 权限控制
  • 第4章 会话与Watcher机制

    • 会话机制
    • Watcher 机制
  • 第5章 ZAB协议与一致性保证

    • ZAB 协议
    • 一致性保证
    • 数据同步
  • 第6章 Leader选举

    • Leader 选举
  • 第7章 典型应用:分布式锁

    • 分布式锁
  • 第8章 典型应用:配置中心与命名服务

    • 配置中心
    • 命名服务
  • 第9章 客户端编程基础(Java原生API)

    • Java 原生 API 编程
  • 第10章 运维与监控

    • 四字命令
    • 监控体系
  • 第11章 面试考点与设计思想

    • 设计思想
    • 面试考点汇编

本章定位:Watcher 是 ZooKeeper 实现分布式协调的"神经末梢"。它允许客户端监听 ZNode 的变化,是实现配置中心、服务发现和分布式锁的基础。

定义与作用

Watcher 是 ZooKeeper 提供的事件通知机制。客户端可以对 ZNode 注册 Watcher,当 ZNode 发生指定事件时,服务端向客户端推送通知。

Watcher 的核心设计特点:

特性说明
一次性触发(One-time Trigger)Watcher 触发后自动取消,需重新注册
异步通知通知不包含变化后的数据,需再次 getData
顺序保证通知先于该事件之后的所有数据变更到达
Session 绑定Session 过期时所有 Watcher 失效

核心原理

Watcher 通知流程

图中展示了 Watcher 的三个关键步骤:注册 → 触发 → 重新注册。通知中不包含数据,客户端必须主动拉取。

支持的 Watcher 类型

注册方法可监听的路径触发事件
exists路径本身Created, Deleted, DataChanged
getData路径本身Deleted, DataChanged
getChildren子节点列表Deleted, ChildrenChanged

持久递归 Watcher(Persistent Recursive Watcher)—— 3.6.0+

ZooKeeper 3.6.0 引入了持久递归 Watcher,解决了标准 Watcher 的两大痛点:①一次性触发后需要手动重新注册;②无法一次性监听整棵子树。它是 Curator TreeCache 的底层依赖能力,让配置中心等需要监听整棵子树的场景变得极其简洁。

核心区别:

特性标准 Watcher持久递归 Watcher
触发后是否删除是(一次性)否(持久化,一直监听)
触发事件NodeCreated/Deleted/DataChanged/ChildrenChangedNodeCreated/Deleted/DataChanged(不含 NodeChildrenChanged)
监听范围单个 ZNode 或直接子节点列表递归监听整棵子树
APIgetData(..., watcher) / exists(..., watcher)addWatch(path, mode)
移除方式触发后自动移除 / Session 过期显式调用 removeWatches
引入版本所有版本3.6.0+

标准 Watcher vs 持久递归 Watcher 的触发行为差异:

关键差异:标准 Watcher 触发一次后"死亡";持久递归 Watcher 持续存活,直到显式 removeWatches 或 Session 过期。

addWatch API 用法:

# 命令行
[zkshell: 0] addWatch /config PERSISTENT_RECURSIVE
# 成功,对 /config 及其所有子节点注册持久递归 Watcher
// Java 原生 API(ZooKeeper 3.6.0+)
zk.addWatch("/config", event -> {
    System.out.println("Event on: " + event.getPath() +
            ", type: " + event.getType());
}, AddWatchMode.PERSISTENT_RECURSIVE);

适用场景:

场景用法说明
配置中心addWatch(/config, PERSISTENT_RECURSIVE)监听整个配置子树,任何配置项新增/修改/删除都感知
服务注册中心addWatch(/services, PERSISTENT_RECURSIVE)监听所有服务的上下线,无需对每个服务单独注册
分布式锁监控addWatch(/locks, PERSISTENT_RECURSIVE)监听锁节点的创建和删除

命令示例:配置子树的递归监听:

# Terminal 1 —— 注册持久递归 Watcher
[zkshell: 0] addWatch /config PERSISTENT_RECURSIVE

# Terminal 2 —— 修改不同层级的节点
[zkshell: 0] set /config/db/host "db2.internal"
[zkshell: 1] create /config/new-app/feature-flag "true"

# Terminal 1 依次收到通知
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/config/db/host
WATCHER::
WatchedEvent state:SyncConnected type:NodeCreated path:/config/new-app/feature-flag
# Watcher 仍然存活,无需重新注册

removeWatches 操作:

# 移除指定路径的持久递归 Watcher
[zkshell: 0] removeWatches /config PERSISTENT_RECURSIVE
// Java API
zk.removeWatches("/config", watcher,
        Watcher.WatcherType.Any, true);  // local=true 仅移除本地

注意:removeWatches 默认移除全局(服务端 + 客户端),设置 local=true 则仅移除客户端记录、不通知服务端清理。

易错场景:事件风暴

持久递归 Watcher 在节点频繁创建/删除时可能引发事件风暴。假设 /locks 下有 100 个临时节点每秒有 50 个被创建和 50 个被删除——持久递归 Watcher 会将每一个事件推送给客户端,瞬时产生每秒 100 条通知。在这种高频写入场景下,建议混合策略:根节点用持久递归 Watcher 感知拓扑变化,叶子节点用标准 Watcher 按需监听。

完整示例

示例一:配置中心 Watcher 循环监听

场景说明:监控 /app/config 节点的数据变更,实现配置热更新。

# Terminal 1 —— 注册 Watcher 并读取
zkCli.sh -server 127.0.0.1:2181
[zkshell: 0] get -w /app/config
port=8080
# Watcher 已注册
# Terminal 2 —— 修改数据
zkCli.sh -server 127.0.0.1:2181
[zkshell: 0] set /app/config "port=9090"
# Terminal 1 收到通知
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/app/config

# 重新注册 Watcher 并获取新数据
[zkshell: 1] get -w /app/config
port=9090

操作前后对比:

时间点Terminal 1 状态/app/config 数据
初始化 get -w读取 port=8080port=8080
Terminal 2 set收到 NodeDataChangedport=9090
重新 get -w读取 port=9090port=9090

示例二:子节点 Watcher — 服务发现

场景说明:监控 /services 下子节点变化,感知服务上下线。

# Terminal 1 —— 监控服务注册中心
zkCli.sh -server 127.0.0.1:2181
[zkshell: 0] ls -w /services
[order-service-1, order-service-2]
# Watcher 已注册在子节点列表上
# Terminal 2 —— 新服务上线
zkCli.sh -server 127.0.0.1:2181
[zkshell: 0] create -e /services/order-service-3 "http://192.168.1.12:8080"
# Terminal 1 收到通知
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/services

# 重新注册并获取最新列表
[zkshell: 1] ls -w /services
[order-service-1, order-service-2, order-service-3]

操作前后对比:

事件/services 子节点列表通知
初始注册[order-service-1, order-service-2]—
新服务上线[order-service-1, order-service-2, order-service-3]NodeChildrenChanged
重新注册同上Watcher 续期

示例三:Java API 持续监听

场景说明:实现一个不中断的配置监听器。

public class ConfigWatcher implements Watcher {
    private ZooKeeper zk;
    private String configPath;

    public ConfigWatcher(ZooKeeper zk, String path) {
        this.zk = zk;
        this.configPath = path;
    }

    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == EventType.NodeDataChanged) {
            try {
                byte[] data = zk.getData(configPath, this, null);
                System.out.println("Config changed: " +
                        new String(data));
                // 已在 getData 中重新注册了 this Watcher
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void start() throws Exception {
        byte[] data = zk.getData(configPath, this, null);
        System.out.println("Initial config: " +
                new String(data));
    }
}

// 使用
ZooKeeper zk = new ZooKeeper("127.0.0.1:2181", 3000, event -> {});
new ConfigWatcher(zk, "/app/config").start();
Thread.sleep(Long.MAX_VALUE);

执行结果(每次配置变化时输出):

Initial config: port=8080
Config changed: port=9090
Config changed: port=9090,debug=true

操作前后对比:

次数/app/config 变化监听器输出Watcher 状态
初始port=8080Initial config: port=8080已注册
第 1 次变更port=9090Config changed: port=9090自动续期
第 2 次变更port=9090,debug=trueConfig changed: port=9090,debug=true自动续期

易错场景与面试考点

易错场景

1. 忘记重新注册 Watcher

// 错误:Watcher 触发后失效,不再监听后续变更
byte[] data = zk.getData("/config", watcher, null);

// process 中直接使用 data,忘记重新 getData
public void process(WatchedEvent event) {
    if (event.getType() == EventType.NodeDataChanged) {
        // data 是旧的!需要重新 getData
    }
}

2. Watcher 事件与数据不一致

Watcher 通知是异步的,客户端收到 NodeDataChanged 后重新 getData,可能再次获得新值(如果期间又被修改过)。这是正常行为,非 BUG。应用层需处理这种"事件合并"。

3. exists Watcher 与 getData Watcher 的区别

# get /node(节点存在时)—— 监听 DataChanged 和 Deleted
get -w /node

# exists /node(节点可能存在也可能不存在)—— 监听 Created、Deleted 和 DataChanged
stat -w /node

如果节点可能不存在但需要监听创建事件,必须使用 exists。

4. Watcher 中的阻塞操作

Watcher 回调在 ZooKeeper 的 EventThread 中执行,不应包含耗时操作。长时间阻塞会导致其他 Watcher 无法被处理。

// 错误:在 Watcher 中进行网络请求
public void process(WatchedEvent event) {
    httpClient.get("http://remote/reload");  // 阻塞 EventThread
}

面试高频题

Q:为什么 Watcher 是一次性的?

A:设计上的权衡。如果 Watcher 持久化,在高频变更场景下会产生大量通知风暴。一次性触发迫使客户端显式重新注册,起到"消费确认"的作用,也避免服务端需要持续追踪和管理大量 Watcher 状态。

Q:Watcher 通知会丢失吗?

A:理论上可能。如果在 Watcher 触发到客户端重新注册之间,发生了多次变更,客户端只会收到一次事件通知(首次触发)。但客户端重新 getData 会获取最新值,不会导致数据最终不一致。

Q:exists 和 getData 的 Watcher 可以同时注册吗?

A:可以,它们互不影响,各自独立触发。例如对同一节点同时注册 exists 和 getData Watcher,数据变更时两个 Watcher 都会触发。

Q:Curator 的 Cache 机制解决了什么痛点?

A:Curator 的 TreeCache / NodeCache 封装了 Watcher 的循环注册逻辑,自动处理重连后的重新注册,提供本地缓存镜像和事件回调。本质上是对原生 Watcher 的高层封装。

小结

要点说明
One-time Trigger标准 Watcher 触发后失效,必须重新注册
持久递归 Watcher3.6.0+ addWatch,持久化且递归监听整棵子树
异步通知不包含数据,需再次 getData
三类注册exists / getData / getChildren
事件类型Created / Deleted / DataChanged / ChildrenChanged
Session 绑定过期后所有 Watcher 失效
通知顺序保证事件先于后续数据变更到达

Watcher 是 ZooKeeper 作为协调服务的核心。下一章进入 ZooKeeper 内部——ZAB 协议与一致性保证,揭示数据如何在集群间同步。

上一页
会话机制