ZK的watch机制

本文详细介绍了Zookeeper的Watcher原理框架,包括客户端、客户端WatchManager和服务器的组成及交互过程。阐述了通知状态与事件类型,如NodeDataChanged、NodeChildrenChanged等。还说明了Watcher的注册过程,涉及多种接口,以及客户端和服务器端的注册步骤和触发回调机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.watcher原理框架


由图看出,zk的watcher由客户端,客户端WatchManager,zk服务器组成。整个过程涉及了消息通信及数据存储。

  • zk客户端向zk服务器注册watcher的同时,会将watcher对象存储在客户端的watchManager。
  • Zk服务器触发watcher事件后,会向客户端发送通知,客户端线程从watchManager中回调watcher执行相应的功能。

有木有看到小红旗?加入小红旗是一个watcher,当小红旗被创建并注册到node1节点(会有相应的API实现)后,就会监听node1+node_a+node_b或node_a+node_b。这里两种情况是因为在创建watcher注册时会有多种途径。并且watcher不能监听到孙节点。注意,watcher设置后,一旦触发一次后就会失效,如果要想一直监听,需要在process回调函数里重新注册相同的 watcher。

2.通知状态与事件

public class WatcherTest implements Watcher {
@Override
public void process(WatchedEvent event) {
// TODO Auto-generated method stub
WatcherTest w = new WatcherTest();
ZooKeeper zk = new ZooKeeper(wx.getZkpath(),10000, w); 
}

public static void main(String[] args){
WatcherTest w = new WatcherTest();
ZooKeeper zk = new ZooKeeper(wx.getZkpath(), 10000, w);
}
}


上面例子是把异常处理,逻辑处理等都省掉。watcher的应用很简单,主要有两步:继承 Watcher 接口,重写 process 回调函数。
当然注册方式有很多,有默认和重新覆盖方式,可以一次触发失效也可以一直有效触发。这些都可以通过代码实现。
2.1 KeeperStatus通知状态
KeeperStatus完整的类名是org.apache.zookeeper.Watcher.Event.KeeperState。
2.2 EventType事件类型
EventType完整的类名是org.apache.zookeeper.Watcher.Event.EventType。

此图是zookeeper常用的通知状态与对应事件类型的对应关系。除了客户端与服务器连接状态下,有多种事件的变化,其他状态的事件都是None。这也是符合逻辑的,因为没有连接服务器肯定不能获取获取到当前的状态,也就无法发送对应的事件类型了。
这里重点说下几个重要而且容易迷惑的事件:

  • NodeDataChanged事件:无论节点数据发生变化还是数据版本发生变化都会触发,即使被更新数据与新数据一样,数据版本dataVersion都会发生变化
  • NodeChildrenChanged:新增节点或者删除节点
  • AuthFailed:重点是客户端会话没有权限而是授权失败

客户端只能收到服务器发过来的相关事件通知,并不能获取到对应数据节点的原始数据及变更后的新数据。因此,如果业务需要知道变更前的数据或者变更后的新数据,需要业务保存变更前的数据(本机数据结构、文件等)和调用接口获取新的数据

3.watcher注册过程
3.1涉及接口
创建zk客户端对象实例时注册:

  • ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
  • ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, boolean canBeReadOnly)
  • ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd)
  • ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd, boolean canBeReadOnly)

通过这种方式注册的watcher将会作为整个zk会话期间的默认watcher,会一直被保存在客户端ZK WatchManager 的 defaultWatcher 中,如果这个被创建的节点在其它时候被创建watcher并注册,则这个默认的watcher会被覆盖。注意,watcher触发一次就会失效,不管是创建节点时的 watcher 还是以后创建的 watcher。
其他注册watcher的API:

  • getChildren(String path, boolean watch):Boolean watch表示是否使用上下文中默认的watcher,即创建zk实例时设置的watcher
  • getData(String path, boolean watch, Stat stat):Boolean watch表示是否使用上下文默认的watcher,即创建zk实例时设置的watcher
  • getData(String path, Watcher watcher, AsyncCallback.DataCallback cb, Object ctx)
  • exists(String path, boolean watch)“Boolean watch表示是否使用上下文中默认的watcher,即创建zk实例时设置的watcher
  • exists(String path, Watcher watcher)

举栗子

 

 

 

 

这就是watcher的简单例子,zk的实际应用集群管理,发布订阅等复杂功能其实就在这个小例子上拓展的。
3.2客户端注册

这里的客户端注册主要是把上面第一点的zookeeper原理框架的注册步骤展开,简单来说就是zk客户端在注册时会先向zk服务器请求注册,服务器会返回请求响应,如果响应成功则zk服务端把watcher对象放到客户端的WatchManager管理并返回响应给客户端。
3.3服务器端注册

FinalRequestProcessor:
/**
* This Request processor actually applies any transaction associated with a
* request and services any queries. It is always at the end of a
* RequestProcessor chain (hence the name), so it does not have a nextProcessor
* member.
*
* This RequestProcessor counts on ZooKeeperServer to populate the
* outstandingRequests member of ZooKeeperServer.
*/
public class FinalRequestProcessor implements RequestProcessor

由源码注释得知,FinalRequestProcessor类实际是任何事务请求和任何查询的的最终处理类。也就是我们客户端对节点的set/get/delete/create/exists等操作最终都会运行到这里。
以exists函数为例子:

case OpCode.exists: {
lastOp = "EXIS";
// TODO we need to figure out the security requirement for this!
ExistsRequest existsRequest = new ExistsRequest();
ByteBufferInputStream.byteBuffer2Record(request.request,
existsRequest);
String path = existsRequest.getPath();
if (path.indexOf('\0') != -1) {
throw new KeeperException.BadArgumentsException();
}
Stat stat = zks.getZKDatabase().statNode(path, existsRequest
.getWatch() ? cnxn : null);
rsp = new ExistsResponse(stat);
break;
}
existsRequest.getWatch() ? cnxn : null此句是在调用exists API时,判断是否注册watcher,若是就返回 cnxn,cnxn是由此句代码ServerCnxn cnxn = request.cnxn;创建的。
/**
* Interface to a Server connection - represents a connection from a client
* to the server.
*/
public abstract class ServerCnxn implements Stats, Watcher

通过ServerCnxn类的源码注释得知,ServerCnxn是维持服务器与客户端的tcp连接与实现了 watcher。总的来说,ServerCnxn类创建的对象cnxn即包含了连接信息又包含watcher信息。
同时仔细看ServerCnxn类里面的源码,发现有以下这个函数,process函数正是watcher的回调函数啊。

public abstract class ServerCnxn implements Stats, Watcher {
.
.
public abstract void process(WatchedEvent event);
Stat stat = zks.getZKDatabase().statNode(path, existsRequest.getWatch() ? cnxn : null); 
//getZKDatabase实际上是获取是在zookeeper运行时的数据库。请看下面
.
.
}

ZKDatabase:

/**
* This class maintains the in memory database of zookeeper
* server states that includes the sessions, datatree and the
* committed logs. It is booted up after reading the logs
* and snapshots from the disk.
*/
public class ZKDatabase
通过源码注释得知ZKDatabase是在zookeeper运行时的数据库,在FinalRequestProcessor的case exists中会把existsRequest(exists请求传递给ZKDatabase)。
/**
* the datatree for this zkdatabase
* @return the datatree for this zkdatabase
*/
public DataTree getDataTree() {
return this.dataTree;
}

ZKDatabase里面有这关键的一个函数是从zookeeper运行时展开的节点数型结构中搜索到合适的节点返回。
watchManager

  • Zk服务器端Watcher的管理者
  • 从两个维度维护watcher
  • watchTable从数据节点的粒度来维护
  • watch2Paths从watcher的粒度来维护
  • 负责watcher事件的触发
class WatchManager {
private final Map<String, Set<Watcher>> watchTable =
new HashMap<String, Set<Watcher>>();

private final Map<Watcher, Set<String>> watch2Paths = new HashMap<Watcher, Set<String>>();
Set<Watcher> triggerWatch(String path, EventType type) { return triggerWatch(path, type, null);}
}

watcher触发

public Stat setData(String path, byte data[], int version, long zxid,long time) throws KeeperException.NoNodeException {
Stat s = new Stat();
DataNode n = nodes.get(path);
if (n == null) {
throw new KeeperException.NoNodeException();
}
byte lastdata[] = null;
synchronized (n) {
lastdata = n.data;
n.data = data;
n.stat.setMtime(time);
n.stat.setMzxid(zxid);
n.stat.setVersion(version);
n.copyStat(s);
}
// now update if the path is in a quota subtree.
String lastPrefix = getMaxPrefixWithQuota(path);
if(lastPrefix != null) {
this.updateBytes(lastPrefix, (data == null ? 0 : data.length)
- (lastdata == null ? 0 : lastdata.length));
}
dataWatches.triggerWatch(path, EventType.NodeDataChanged); //触发事件
return s;
}

客户端回调watcher步骤:

  • 反序列化,将孒节流转换成WatcherEvent对象。因为在Java中网络传输肯定是使用了序列化的,主要是为了节省网络IO和提高传输效率。
  • 处理chrootPath。获取节点的根节点路径,然后再搜索树而已。
  • 还原watchedEvent:把WatcherEvent对象转换成WatchedEvent。主要是把zk服务器那边的WatchedEvent事件变为WatcherEvent,标为已watch触发。
  • 回调Watcher:把WatchedEvent对象交给EventThread线程。EventThread线程主要是负责从客户端的ZKWatchManager中取出Watcher,并放入waitingEvents队列中,然后供客户端获取。

转载于:https://www.cnblogs.com/ericz2j/p/11168669.html

<think>好的,用户想了解ZooKeeper中的Watch机制,包括工作原理、使用方法和示例。首先,我需要回忆一下ZooKeeper的基础知识,尤其是Watch机制的相关内容。记得Watch机制是ZooKeeper实现分布式协调的核心功能之一,它允许客户端在数据节点(znode)发生变化时接收通知。 接下来,我需要整理Watch机制的工作原理。根据已有的引用资料,比如引用[1]提到每次监视到事件后需要再次注册,这说明Watch是一次性的,触发后需要重新设置。引用[4]也提到通过注册Watcher来监控锁的状态,这涉及到事件类型和通知机制。因此,工作原理部分应该包括一次性触发、事件类型、异步通知和客户端处理这几个方面。 然后是使用方法,用户可能想知道如何在代码中注册Watcher。引用[2]和[3]提到使用Python的kazoo库,比如通过exists方法添加Watch,并在回调函数中处理事件。需要分步骤说明,比如注册方式、事件类型和处理逻辑,同时强调需要重新注册Watch。 示例部分,可以用Python的kazoo库来展示,比如监控节点数据变化。代码示例应该包括连接ZooKeeper、定义回调函数、注册Watch,并保持连接活跃。引用[3]中的代码片段可以作为参考,但需要确保语法正确,并添加注释说明。 还要考虑用户可能遇到的问题,比如Watch丢失的情况,或者如何保证数据一致性,这些在引用[2]和[4]中都有涉及。需要提醒用户注意事项,如一次性特性、网络延迟和事件顺序性。 最后生成相关问题,覆盖原理、应用场景、与回调的关系、丢失处理以及与其他系统的对比,这样用户可以有进一步学习的路径。同时,要确保引用标记正确,比如在提到需要重新注册Watch的地方添加[^1]和[^4]。 总结一下,回答的结构应该分为原理、使用方法、示例、注意事项和相关问题,每个部分引用对应的资料来支持内容,确保信息准确可靠。同时,保持语言简洁明了,符合用户的中文需求,避免使用复杂术语,必要时提供代码示例帮助理解。</think>### ZooKeeper Watch机制解析 #### 一、核心原理 1. **一次性触发** Watch监听在znode节点变更后仅触发一次,后续变更需重新注册[^1]。例如监控节点`/service`的数据变化,首次数据更新会触发通知,但后续变化需再次调用`exists()`或`getData()`重新设置Watch。 2. **事件类型与异步通知** 支持节点创建($\text{NodeCreated}$)、删除($\text{NodeDeleted}$)、数据变更($\text{NodeDataChanged}$)、子节点变化($\text{NodeChildrenChanged}$)四类事件。通知通过独立的回调线程异步传递。 3. **通知与状态同步机制** 事件通知仅包含节点路径和事件类型,不携带变更内容,客户端需主动查询新数据[^2]。例如: ```python def watcher_callback(event): if event.type == 'CHANGED': new_data = zk.get(event.path, watch=watcher_callback)[0] # 重新注册Watch ``` #### 二、使用方法 1. **注册方式** - 通过`exists(path, watch=True)`监控节点存在性 - 通过`getData(path, watch=True)`监控数据变化 - 通过`getChildren(path, watch=True)`监控子节点变化 2. **Python实现示例** ```python from kazoo.client import KazooClient zk = KazooClient(hosts='127.0.0.1:2181') zk.start() def data_change_watcher(event): print(f"节点 {event.path} 数据已变更,事件类型:{event.type}") # 必须重新注册Watch zk.get(event.path, watch=data_change_watcher) # 初始注册Watch zk.get('/config/server1', watch=data_change_watcher) ``` #### 三、典型应用场景 1. **分布式锁实现** 通过监控`/lock`节点的子节点变化,实现锁竞争通知。 2. **服务发现系统** 监控`/services`子节点变化,实时更新可用服务列表。 3. **配置中心** 监控`/config`节点数据变更,实现配置热更新。 #### 四、注意事项 1. **事件丢失风险** 在Watch触发到重新注册期间发生的变更可能无法捕获,需配合版本号校验: ```python data, stat = zk.get('/config', watch=watcher) # 后续操作使用stat.version进行版本验证 ``` 2. **网络分区处理** 会话过期($\text{SessionExpired}$)事件需特殊处理,此时所有Watch将被清除。 $$ \text{事件可靠性} = \begin{cases} \text{强保障} & \text{在watch生效期间} \\ \text{需补偿机制} & \text{会话中断后} \end{cases} $$
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值