Zookeeper之Watcher机制详解

概念

  • Zookeeper提供了数据的发布/订阅功能。多个订阅者可监听某一特定主题对象(节点)。当主题对象发生改变(数据内容改变,被删除等),会实时通知所有订阅者。
  • Zookeeper采用了Watcher机制实现数据的发布/订阅功能。该机制在被订阅对象发生变化时,异步通知客户端,因此客户端不必在注册监听后轮询阻塞。
  • Watcher机制实际上与观察者模式类似,也可看作观察者模式在分布式场景中给的一种应用。

特性

特性 说明
一次性 Watcher是一次性的,一旦出发,就会被移除,再次使用需要重新注册
客户端顺序回调 多个注册器的回调是顺序串行执行的,只有回调后客户端才能看到最新的数据状态,一个Watcher的回调逻辑不应该太多,以免影响别的watcher的执行
轻量级 WatcherEvent是最小的通信单元,结构上只包含连接状态、事件类型和节点路径,并不会告诉数据节点变化前后的具体情况
时效性 watcher只有在当前session彻底时效时才会无效,弱session有效期内重新连接成功,则watcher依然存在

ZooKeeper中的读取操作getData、exist、getChildren。 等都可以使用指定参数为节点设置监听。这是ZooKeeper对监听的定义:监听事件是一次性触发事件,当某客户端会话连接监听了某个节点,当该节点被修改时,就会触发事件,通知客户端。只会触发一次,如果想要继续触发,就要重新监听该节点。

Zookeeper监听有三个关键点:

  1. 客户端对该节点注册监听,也就是客户端对该节点进行订阅。
  2. 该节点发生改变,触发某一事件后,客户端会收到一个回调。可以执行相应回调执行相应动作。
  3. 注册的监听只会生效一次,要想继续生效,就要在回调的方法中继续注册监听。

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秒。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章