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

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