http://blog.csdn.net/lipeng_bigdata/article/details/50985993
Watch是ZooKeeper中非常重要的一个机制,它可以监控ZooKeeper中节点的变化情况,告知客户端。下面,我们以代码为例来分析Watch在ZooKeeper中是如何实现的。ZooKeeper中一共由三种方法可以实现Watch,分别为getData、exists和getChildren,今天我们先来看下exists()方法:
2、exists()
import java.io.IOException;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.data.Stat;
public class TestZooKeeperWatcher {
public static void main(String[] args) {
ZooKeeper zk = null;
try {
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("开始连接ZooKeeper...");
// 创建与ZooKeeper服务器的连接zk
String address = "192.168.1.226:2181";
int sessionTimeout = 3000;
zk = new ZooKeeper(address, sessionTimeout, new Watcher() {
// 监控所有被触发的事件
public void process(WatchedEvent event) {
if (event.getType() == null || "".equals(event.getType())) {
return;
}
System.out.println("已经触发了" + event.getType() + "事件!");
}
});
System.out.println("ZooKeeper连接创建成功!");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
// 创建根目录节点
// 路径为/tmp_root_path
// 节点内容为字符串"我是根目录/tmp_root_path"
// 创建模式为CreateMode.PERSISTENT
System.out.println("开始创建根目录节点/tmp_root_path...");
zk.create("/tmp_root_path", "我是根目录/tmp_root_path".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("根目录节点/tmp_root_path创建成功!");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
// 创建第一个子目录节点
// 路径为/tmp_root_path/childPath1
// 节点内容为字符串"我是第一个子目录/tmp_root_path/childPath1"
// 创建模式为CreateMode.PERSISTENT
System.out.println("开始创建第一个子目录节点/tmp_root_path/childPath1...");
zk.create("/tmp_root_path/childPath1",
"我是第一个子目录/tmp_root_path/childPath1".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("第一个子目录节点/tmp_root_path/childPath1创建成功!");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
// 创建第二个子目录节点
// 路径为/tmp_root_path/childPath2
// 节点内容为字符串"我是第二个子目录/tmp_root_path/childPath2"
// 创建模式为CreateMode.PERSISTENT
System.out.println("开始创建第二个子目录节点/tmp_root_path/childPath2...");
zk.create("/tmp_root_path/childPath2",
"我是第二个子目录/tmp_root_path/childPath2".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("第二个子目录节点/tmp_root_path/childPath2创建成功!");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
// 修改第一个子目录节点/tmp_root_path/childPath1数据
System.out.println("开始修改第一个子目录节点/tmp_root_path/childPath1数据...");
zk.setData("/tmp_root_path/childPath1",
"我是修改数据后的第一个子目录/tmp_root_path/childPath1".getBytes(), -1);
System.out.println("修改第一个子目录节点/tmp_root_path/childPath1数据成功!");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
// 修改第二个子目录节点/tmp_root_path/childPath2数据
System.out.println("开始修改第二个子目录节点/tmp_root_path/childPath2数据...");
zk.setData("/tmp_root_path/childPath2",
"我是修改数据后的第二个子目录/tmp_root_path/childPath2".getBytes(), -1);
System.out.println("修改第二个子目录节点/tmp_root_path/childPath2数据成功!");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
// 获取根目录节点状态
System.out.println("开始获取根目录节点状态...");
System.out.println(zk.exists("/tmp_root_path", true));
System.out.println("根目录节点状态获取成功");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
// 删除第一个子目录节点
System.out.println("开始删除第一个子目录节点/tmp_root_path/childPath1...");
zk.delete("/tmp_root_path/childPath1", -1);
System.out.println("第一个子目录节点/tmp_root_path/childPath1删除成功!");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
// 删除第二个子目录节点
System.out.println("开始删除第二个子目录节点/tmp_root_path/childPath2...");
zk.delete("/tmp_root_path/childPath2", -1);
System.out.println("第二个子目录节点/tmp_root_path/childPath2删除成功!");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
// 删除根目录节点
System.out.println("开始删除根目录节点/tmp_root_path...");
zk.delete("/tmp_root_path", -1);
System.out.println("根目录节点/tmp_root_path删除成功!");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
} catch (IOException | KeeperException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
// 关闭连接
if (zk != null) {
try {
zk.close();
System.out.println("释放ZooKeeper连接成功!");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
执行结果如下:
...
...
...
...
开始连接ZooKeeper...
ZooKeeper连接创建成功!
已经触发了None事件!
...
...
...
...
开始创建根目录节点/tmp_root_path...
根目录节点/tmp_root_path创建成功!
...
...
...
...
开始创建第一个子目录节点/tmp_root_path/childPath1...
第一个子目录节点/tmp_root_path/childPath1创建成功!
...
...
...
...
...
...
...
...
开始创建第二个子目录节点/tmp_root_path/childPath2...
第二个子目录节点/tmp_root_path/childPath2创建成功!
...
...
...
...
开始修改第一个子目录节点/tmp_root_path/childPath1数据...
修改第一个子目录节点/tmp_root_path/childPath1数据成功!
...
...
...
...
开始修改第二个子目录节点/tmp_root_path/childPath2数据...
修改第二个子目录节点/tmp_root_path/childPath2数据成功!
...
...
...
...
开始获取根目录节点状态...
2007741,2007741,1458963342014,1458963342014,0,2,0,0,29,2,2007743
根目录节点状态获取成功
...
...
...
...
开始删除第一个子目录节点/tmp_root_path/childPath1...
第一个子目录节点/tmp_root_path/childPath1删除成功!
...
...
...
...
开始删除第二个子目录节点/tmp_root_path/childPath2...
第二个子目录节点/tmp_root_path/childPath2删除成功!
...
...
...
...
开始删除根目录节点/tmp_root_path...
已经触发了NodeDeleted事件!
根目录节点/tmp_root_path删除成功!
...
...
...
...
释放ZooKeeper连接成功!
而如果我们在获取根目录节点状态之后,修改根目录数据,添加代码和执行结果如下:
// 修改根目录节点数据
System.out.println("开始修改根目录节点/tmp_root_path数据...");
zk.setData("/tmp_root_path",
"我是修改数据后的根目录/tmp_root_path".getBytes(), -1);
System.out.println("修改根目录节点/tmp_root_path数据成功!");
Thread.currentThread().sleep(1000l);
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
...
...
...
...
开始连接ZooKeeper...
ZooKeeper连接创建成功!
已经触发了None事件!
...
...
...
...
开始创建根目录节点/tmp_root_path...
根目录节点/tmp_root_path创建成功!
...
...
...
...
开始创建第一个子目录节点/tmp_root_path/childPath1...
第一个子目录节点/tmp_root_path/childPath1创建成功!
...
...
...
...
...
...
...
...
开始创建第二个子目录节点/tmp_root_path/childPath2...
第二个子目录节点/tmp_root_path/childPath2创建成功!
...
...
...
...
开始修改第一个子目录节点/tmp_root_path/childPath1数据...
修改第一个子目录节点/tmp_root_path/childPath1数据成功!
...
...
...
...
开始修改第二个子目录节点/tmp_root_path/childPath2数据...
修改第二个子目录节点/tmp_root_path/childPath2数据成功!
...
...
...
...
开始获取根目录节点状态...
2007775,2007775,1458963528012,1458963528012,0,2,0,0,29,2,2007777
根目录节点状态获取成功
...
...
...
...
开始修改根目录节点/tmp_root_path数据...
已经触发了NodeDataChanged事件!
修改根目录节点/tmp_root_path数据成功!
...
...
...
...
开始删除第一个子目录节点/tmp_root_path/childPath1...
第一个子目录节点/tmp_root_path/childPath1删除成功!
...
...
...
...
开始删除第二个子目录节点/tmp_root_path/childPath2...
第二个子目录节点/tmp_root_path/childPath2删除成功!
...
...
...
...
开始删除根目录节点/tmp_root_path...
根目录节点/tmp_root_path删除成功!
...
...
...
...
释放ZooKeeper连接成功!
与getData()一致!
结论:
exists()方法仅仅监控对应节点的一次数据变化,无论是数据修改还是删除!若要每次对应节点发生变化都被监测到,那么每次都得先调用exists()方法获取一遍节点状态!
1. zookeeper的Watch机制
一个zk的节点可以被监控,包括这个目录中存储的数据的修改,子节点目录的变化,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的集中管理,集群管理,分布式锁等等。
watch机制官方说明:一个Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端,以便通知它们。
2. zookeeper机制的特点
1) 一次性的触发器(one-time trigger)
当数据改变的时候,那么一个Watch事件会产生并且被发送到客户端中。但是客户端只会收到一次这样的通知,如果以后这个数据再次发生改变的时候,之前设置Watch的客户端将不会再次收到改变的通知,因为Watch机制规定了它是一个一次性的触发器。
当设置监视的数据发生改变时,该监视事件会被发送到客户端,例如,如果客户端调用了 getData("/znode1", true) 并且稍后 /znode1 节点上的数据发生了改变或者被删除了,客户端将会获取到 /znode1 发生变化的监视事件,而如果 /znode1 再一次发生了变化,除非客户端再次对 /znode1 设置监视,否则客户端不会收到事件通知。
2)发送给客户端(Sent to the client)
这个表明了Watch的通知事件是从服务器发送给客户端的,是异步的,这就表明不同的客户端收到的Watch的时间可能不同,但是ZooKeeper有保证:当一个客户端在看到Watch事件之前是不会看到结点数据的变化的。例如:A=3,此时在上面设置了一次Watch,如果A突然变成4了,那么客户端会先收到Watch事件的通知,然后才会看到A=4。
Zookeeper 客户端和服务端是通过 Socket 进行通信的,由于网络存在故障,所以监视事件很有可能不会成功地到达客户端,监视事件是异步发送至监视者的,Zookeeper 本身提供了保序性(ordering guarantee):即客户端只有首先看到了监视事件后,才会感知到它所设置监视的 znode 发生了变化(a client will never see a change for which it has set a watch until it first sees the watch event). 网络延迟或者其他因素可能导致不同的客户端在不同的时刻感知某一监视事件,但是不同的客户端所看到的一切具有一致的顺序。
3)被设置Watch的数据(The data for which the watch was set)
这意味着 znode 节点本身具有不同的改变方式。你也可以想象 Zookeeper 维护了两条监视链表:
数据监视和子节点监视(data watches and child watches)
getData() and exists() 设置数据监视,getChildren() 设置子节点监视。 或者,你也可以想象 Zookeeper 设置的不同监视返回不同的数据,getData() 和 exists() 返回 znode 节点的相关信息,而 getChildren() 返回子节点列表。
因此, setData() 会触发设置在某一节点上所设置的数据监视(假定数据设置成功),而一次成功的 create() 操作则会出发当前节点上所设置的数据监视以及父节点的子节点监视。一次成功的 delete() 操作将会触发当前节点的数据监视和子节点监视事件,同时也会触发该节点父节点的child watch。
3.各种watch触发的情况总结
可以注册watcher的方法:getData、exists、getChildren。
可以触发watcher的方法:create、delete、setData。连接断开的情况下触发的watcher会丢失。
一个Watcher实例是一个回调函数,被回调一次后就被移除了。如果还需要关注数据的变化,需要再次注册watcher。
New ZooKeeper时注册的watcher叫default watcher,它不是一次性的,只对client的连接状态变化作出反应。
什么样的操作会产生什么类型的事件:
event For “/path” | event For “/path/child” | |
create(“/path”) | EventType.NodeCreated | 无 |
delete(“/path”) | EventType.NodeDeleted | 无 |
setData(“/path”) | EventType.NodeDataChanged | 无 |
create(“/path/child”) | EventType.NodeChildrenChanged(getChild) | EventType.NodeCreated |
delete(“/path/child”) | EventType.NodeChildrenChanged(getChild) | EventType.NodeDeleted |
setData(“/path/child”) | 无 | EventType.NodeDataChanged |
事件类型与watcher的对应关系:
event For “/path” | Default Watcher |
exists(“/path”) |
getData(“/path”) |
getChildren(“/path”) |
EventType.None | √ | √ | √ | √ |
EventType.NodeCreated | √ | √ | ||
EventType.NodeDeleted | √ | √ | ||
EventType.NodeDataChanged | √ | √ | ||
EventType.NodeChildrenChanged | √ |
本表总结:exits和getData设置数据监视,而getChildren设置子节点监视
操作与watcher的对应关系:
exits("/path") | getData(“/path”) | getChildren(“/path”) | exits("/path/child") | getData(“/path/child”) | getChildren(“/path/child”) | |
create(“/path”) | √ | √ | 会报错 | |||
delete(“/path”) | √ | √ | √(这个要注意) | |||
setData(“/path”) | √ | √ | ||||
create(“/path/child”) | √ | √ | √ | |||
delete(“/path/child”) | √ | √ | √ | √ | ||
setData(“/path/child”) | √ | √ | ||||
值得注意的是:getChildren("/path")监视/path的子节点,如果(/path)自己删了,也会触发NodeDeleted事件。
4.实现永久监听
由于zookeeper是一次性监听,所以我们必须在wather的process方法里面再设置监听。一个方法如下:
以下逻辑是实现的是生产者和消费者模型,消费者监听某一路径下面子节点的变化,当生产者有消息发送过来的时候,在该节点下面创建一个子节点,然后把消息放到该子节点里面,这会触发消费者的process方法被调用,然后消费者取到该节点下面的子节点(顺便设置一个再监听该节点的子节点),然后取出子节点的内容,做处理,然后删除该子节点。
public void process(WatchedEvent event) { // TODO Auto-generated method stub if (event.getState() == KeeperState.SyncConnected) { System.out.println("watcher received event"); countDownLatch.countDown(); } System.out.println("回调watcher1实例: 路径" + event.getPath() + " 类型:"+ event.getType()); // 事件类型,状态,和检测的路径 EventType eventType = event.getType(); KeeperState state = event.getState(); String watchPath = event.getPath(); switch (eventType) { case NodeCreated: break; case NodeDataChanged: break; case NodeChildrenChanged: try { //处理收到的消息 handleMessage(watchPath); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (KeeperException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } break; default: break; } } public void handleMessage(String watchPath) throws KeeperException,InterruptedException, UnsupportedEncodingException { System.out.println("收到消息"); //再监听该子节点 ListChildren = this.getChildren(watchPath); for (String a : Children) { String childrenpath = watchPath + "/" + a; byte[] recivedata = this.getData(childrenpath); String recString = new String(recivedata, "UTF-8"); System.out.println("receive the path:" + childrenpath + ":data:"+ recString); //做完了之后,删除该节点 this.deletNode(childrenpath, -1); } } public List getChildren(String path) throws KeeperException,InterruptedException { //监听该节点子节点的变化情况 return this.zooKeeper.getChildren(path, this); } public Stat setData(String path, byte[] data, int version)throws KeeperException, InterruptedException { return this.zooKeeper.setData(path, data, version); }