概念
- Zookeeper提供了数据的发布/订阅功能。多个订阅者可监听某一特定主题对象(节点)。当主题对象发生改变(数据内容改变,被删除等),会实时通知所有订阅者。
- Zookeeper采用了Watcher机制实现数据的发布/订阅功能。该机制在被订阅对象发生变化时,异步通知客户端,因此客户端不必在注册监听后轮询阻塞。
- Watcher机制实际上与观察者模式类似,也可看作观察者模式在分布式场景中给的一种应用。
特性
特性 | 说明 |
---|---|
一次性 | Watcher是一次性的,一旦出发,就会被移除,再次使用需要重新注册 |
客户端顺序回调 | 多个注册器的回调是顺序串行执行的,只有回调后客户端才能看到最新的数据状态,一个Watcher的回调逻辑不应该太多,以免影响别的watcher的执行 |
轻量级 | WatcherEvent是最小的通信单元,结构上只包含连接状态、事件类型和节点路径,并不会告诉数据节点变化前后的具体情况 |
时效性 | watcher只有在当前session彻底时效时才会无效,弱session有效期内重新连接成功,则watcher依然存在 |
ZooKeeper中的读取操作getData、exist、getChildren。 等都可以使用指定参数为节点设置监听。这是ZooKeeper对监听的定义:监听事件是一次性触发事件,当某客户端会话连接监听了某个节点,当该节点被修改时,就会触发事件,通知客户端。只会触发一次,如果想要继续触发,就要重新监听该节点。
Zookeeper监听有三个关键点:
- 客户端对该节点注册监听,也就是客户端对该节点进行订阅。
- 该节点发生改变,触发某一事件后,客户端会收到一个回调。可以执行相应回调执行相应动作。
- 注册的监听只会生效一次,要想继续生效,就要在回调的方法中继续注册监听。
java api中 有三个方法可以注册监听,getData、exist、getChildren。
监听器:
接听器接口,我们可以实现该接口实现自定义的监听器注册到节点上。
事件类型可以分为连接事件状态类型和节点事件类型。
事件类型:由Watcher.Event.EventType枚举维护。
主要有5种类型:
NodeCreated:节点被创建时触发。
NodeDeleted:节点被删除时触发。
NodeDataChanged:节点数据被修改时触发。
NodeChildrenChanged:子节点被创建或者删除时触发。
NONE: 该状态就是连接状态事件类型。前面四种是关于节点的事件,这种是连接的事件,具体由Watcher.Event.KeeperState枚举维护。
注册事件的方式与节点事件的关系:
方式 | NodeCreated | NodeDeleted | NodeDataChanged | NodeChildrenChanged |
---|---|---|---|---|
exist | 可监控 | 可监控 | 可监控 | 不可监控 |
getData | 不可监控 | 可监控 | 可监控 | 不可监控 |
getChildren | 不可监控 | 可监控 | 不可监控 | 可监控 |
以上表格是对设置监听的方法对相应的事件是否可监控到。比如exist方法对节点删除事件不可监控,假如用该方法注册监听的话,节点删除时并不会触发事件回调。
连接事件类型
是指客户端连接时会触发的事件,由Watcher.Event.KeeperState枚举维护。
主要有四个类型:
SyncConnected :客户端与服务器正常连接时触发的事件。
Disconnected :客户端与服务器断开连接时触发的事件。
AuthFailed:身份认证失败时触发的事件
Expired :客户端会话Session超时时触发的事件。
测试连接事件:
代码:
public class ZookeeperWatchDemo implements Watcher {
private ZooKeeper zooKeeper;
//路径前缀
private static final String PATH_PREFIX = "/watch";
//相当于发令枪,会阻塞线程,待得Count变为0时,就放开线程。
CountDownLatch latch = new CountDownLatch(1);
@Override
public void process(WatchedEvent watchedEvent) {
//当时间类型是连接状态事件类型是进入
if (watchedEvent.getType().equals(Event.EventType.None)){
if (watchedEvent.getState().equals(Watcher.Event.KeeperState.SyncConnected)){
latch.countDown();
System.out.println("已连接到客户端");
}
if (watchedEvent.getState().equals(Watcher.Event.KeeperState.Expired)){
System.out.println("会话超时");
}
if (watchedEvent.getState().equals(Event.KeeperState.AuthFailed)){
System.out.println("身份认证失败");
}
if (watchedEvent.getState().equals(Event.KeeperState.Disconnected)){
System.out.println("断开连接");
}
}
}
//测试客户端连接事件
@Test
public void testClientConnectZookeeper(){
try {
//连接客户端,设置会话超时时间为5秒
zooKeeper = new ZooKeeper("192.168.18.130:2181",5000,this);
latch.await();
TimeUnit.SECONDS.sleep(120);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
首先运行代码连接到服务器,触发连接事件,打印已连接到客户端。
接着禁用本机与虚拟机的连接网络。
然后客户端与服务器就会断开连接,触发连接断开事件。
在一定时间内(连接超时,过了连接超时时间会关闭连接)重新启用本机与虚拟机网络的连接。客户端会重写连接上服务器,触发连接事件。
首先运行代码连接到服务器,触发连接事件,打印已连接到客户端。
接着禁用本机与虚拟机的连接网络。触发连接断开时间。打印断开连接。
过五秒以上重新启用本机与虚拟机网络的连接。因为客户端连接时设置的会话超时为5秒。超过五秒中重写连接上来会触发会话超时时间。打印会话超时。
@Test
public void testClientConnectZookeeper1(){
try {
zooKeeper = new ZooKeeper("192.168.18.130:2181",50000,this);
latch.await();
//使用错误的添加认证方式
zooKeeper.addAuthInfo("digest12","admin:123456".getBytes());
TimeUnit.SECONDS.sleep(120);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
节点事件测试:
public class ZookeeperWatchDemo implements Watcher {
private ZooKeeper zooKeeper;
//路径前缀
private static final String PATH_PREFIX = "/watch";
//相当于发令枪,会阻塞线程,待得Count变为0时,就放开线程。
CountDownLatch latch = new CountDownLatch(1);
@Override
public void process(WatchedEvent watchedEvent) {
//当时间类型是连接状态事件类型是进入
if (watchedEvent.getType().equals(Event.EventType.None)){
if (watchedEvent.getState().equals(Watcher.Event.KeeperState.SyncConnected)){
latch.countDown();
System.out.println("已连接到客户端");
}
if (watchedEvent.getState().equals(Watcher.Event.KeeperState.Expired)){
System.out.println("会话超时");
}
if (watchedEvent.getState().equals(Event.KeeperState.AuthFailed)){
System.out.println("身份认证失败");
}
if (watchedEvent.getState().equals(Event.KeeperState.Disconnected)){
System.out.println("断开连接");
}
}
else {
if (watchedEvent.getType().equals(Event.EventType.NodeCreated)){
System.out.println("节点被创建");
System.out.println(watchedEvent.getType());
System.out.println(watchedEvent.getPath());
}
if (watchedEvent.getType().equals(Event.EventType.NodeChildrenChanged)){
System.out.println("子节点被修改");
System.out.println(watchedEvent.getType());
System.out.println(watchedEvent.getPath());
}
if (watchedEvent.getType().equals(Event.EventType.NodeDataChanged)){
System.out.println("节点数据被修改");
System.out.println(watchedEvent.getType());
System.out.println(watchedEvent.getPath());
}
if (watchedEvent.getType().equals(Event.EventType.NodeDeleted)){
System.out.println("节点被删除");
System.out.println(watchedEvent.getType());
System.out.println(watchedEvent.getPath());
}
}
}
//测试客户端连接事件
@Test
public void testClientConnectZookeeper(){
try {
zooKeeper = new ZooKeeper("192.168.18.130:2181",50000,this);
latch.await();
//使用错误的添加认证方式
zooKeeper.addAuthInfo("digest12","admin:123456".getBytes());
TimeUnit.SECONDS.sleep(120);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Test
public void testNodeExistEvent(){
try {
zooKeeper = new ZooKeeper("192.168.18.130:2181",50000,this);
latch.await();
//使用Exist方法监控节点
zooKeeper.exists(PATH_PREFIX + "/exist",true);
TimeUnit.SECONDS.sleep(3600);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
}
另一客户端的操作:
结果:
另一客户端进行了节点创建、子节点创建、节点数据修改、节点删除操作,但是只触发了节点创建事件,原因是监听是一次性的,需要在回调方法上重新监听该节点达到重复监听。
子节点创建事件没有触发,说明exist方法监控不到子节点改变事件。
getData方法注册监听:
因为getData方法在节点不存在的情况下会抛出异常,所以天然就不能监控节点创建事件。
@Test
public void testNodeGetDataEvent(){
try {
zooKeeper = new ZooKeeper("192.168.18.130:2181",50000,this);
latch.await();
//使用getData方法监控节点
zooKeeper.getData(PATH_PREFIX + "/getData",true,null);
TimeUnit.SECONDS.sleep(3600);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
子节点创建时子节点改变事件没有发生,所以getData也不能监控子节点改变事件。最后的异常是触发节点删除事件后,在回调函数中尝试使用getData方法注册监听,此时节点已经不存在,所以抛出节点不存在异常。
getChildren方法注册监听
@Test
public void testNodeGetChildrenEvent(){
try {
zooKeeper = new ZooKeeper("192.168.18.130:2181",50000,this);
latch.await();
//使用getChildren方法监控节点
zooKeeper.getChildren(PATH_PREFIX + "/getChildren",true,null);
TimeUnit.SECONDS.sleep(3600);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
结果:
上面操作包含:修改节点数据、创建子节点、修改子节点数据、删除子节点、删除节点。
但是只在 子节点被删除、创建时触发了子节点改变事件,在节点被删除时触发了节点删除事件。
所以用getChildren方法监控不到节点数据被修改事件和节点创建事件,并且字节点创建和删除才会触发子节点修改事件。
自定义监听器
上面的都是使用建立客户端连接时传入的监听器,我们也可以使用特定的监听器。那就要用到各自的重载方法了。这里讲得都是同步的方法,异步方法类似。
参数直接传自定义监听器对象。
public byte[] getData(String path, Watcher watcher, Stat stat)
public List getChildren(String path, Watcher watcher)
public List getChildren(String path, Watcher watcher, Stat stat)
public Stat exists(String path, Watcher watcher)
@Test
public void testNodeCustomEvent(){
try {
zooKeeper = new ZooKeeper("192.168.18.130:2181",50000,this);
latch.await();
//使用getChildren方法监控节点
CustomWatcher watcher = new CustomWatcher(zooKeeper);
CustomWatcher watcher1 =new CustomWatcher(zooKeeper);
zooKeeper.exists(PATH_PREFIX + "/customExist", watcher);
//还可以注册多个监听
zooKeeper.exists(PATH_PREFIX + "/customExist", watcher1);
TimeUnit.SECONDS.sleep(3600);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
public class CustomWatcher implements Watcher{
private ZooKeeper zooKeeper;
public CustomWatcher(ZooKeeper zooKeeper){
this.zooKeeper = zooKeeper;
}
@Override
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getType().equals(Event.EventType.NodeCreated)){
System.out.println("自定义监听节点被创建");
System.out.println(watchedEvent.getType());
System.out.println(watchedEvent.getPath());
}
if (watchedEvent.getType().equals(Event.EventType.NodeChildrenChanged)){
System.out.println("自定义监听子节点被修改");
System.out.println(watchedEvent.getType());
System.out.println(watchedEvent.getPath());
}
if (watchedEvent.getType().equals(Event.EventType.NodeDataChanged)){
System.out.println("自定义监听节点数据被修改");
System.out.println(watchedEvent.getType());
System.out.println(watchedEvent.getPath());
}
if (watchedEvent.getType().equals(Event.EventType.NodeDeleted)){
System.out.println("自定义监听节点被删除");
System.out.println(watchedEvent.getType());
System.out.println(watchedEvent.getPath());
}
try {
zooKeeper.exists(watchedEvent.getPath(),this);
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
注册了两个监听,全执行了。并且回调是顺序执行的。监听回调里睡眠了十秒,而两个回调输出时间相差了10秒。