Zookeeper中的Watcher機制到底是啥?

啥是watcher機制

Zookeeper的watcher機制是其一個非常核心的機制,zookeeper提供的發佈/訂閱,監聽節點變化(如節點的刪除,內容的變化,或者子節點狀態的變化)等核心功能都是基於watcher機制來是是實現的

而watcher機制的實現其實說白了就是一個觀察者模式,只不過這個模式是分佈式的,而不是單機的。

watcher機制涉及狀態的監聽,而這一狀態其實包含了兩種狀態:客戶端與服務端之間的連接狀態(通知狀態)以及節點的狀態(事件類型)

通知狀態(KeeperState)

KeeperState描述的其實就是客戶端和服務端之間的連接狀態發生變化時的一些通知類型,在ZK的Java客戶端中,有一個枚舉專門保存着這些狀態:org.apache.zookeeper.Watcher.Event.KeeperState

一些主要屬性如下:

枚舉屬性名 描述
DisConnected 很好理解,就是CS處於未連接的狀態
SyncConnected 正常連接狀態
Expired 會話超時。客戶端和服務端連接時,服務端會爲其分配一個佔有時間,如果到期了還沒有進行”續約“,那麼就會進行Expired狀態
NoSyncConnected 屬性超時
AuthFailed 身份認證失敗,服務端拒絕連接
ConnectedReadOnly 這個是zookeeper3.3版本開始才提供的模式。如果客戶端設置了允許ReadOnly的話,那麼當集羣中有過半機器出現異常的時候,按照以往的做法,是整個服務直接無法對外提供;而如果設置了ReadOnly的話,就算出現了過半異常,客戶端還可以做只讀

事件類型(EventType)

這個用的其實是更多的,因爲它描述的是Znode節點的狀態變更,來完成一些發佈/訂閱等功能。同樣的,在Java客戶端中,也有一個枚舉與其對應:org.apache.zookeeper.Watcher.Event.EventType

枚舉屬性名 描述
NodeCreated Watcher監聽的節點被創建
NodeDeleted Watcher監聽的節點被刪除
NodeDataChanged Watcher監聽的節點值被修改
NodeChildrenChanged Watcher監聽的節點的子節點狀態變化

EventType註冊與通知之客戶端實現

其實本質上就是三個HashMap,直接上代碼就能看得很清楚

//這些Map的key都是節點路徑,類似於/a,/b;而value則是該節點對應的所有watch

//節點內容監聽
private final Map<String, Set<Watcher>> dataWatches = new HashMap<String, Set<Watcher>>();

//代表節點狀態變更(創建或銷燬)監聽
private final Map<String, Set<Watcher>> existWatches = new HashMap<String, Set<Watcher>>();

//節點的子節點狀態監聽
private final Map<String, Set<Watcher>> childWatches = new HashMap<String, Set<Watcher>>();

EventType註冊與通知之服務端實現

同樣的,也是有兩個map與之相關,直接貼代碼

/*
* 這裏的key代表節點路徑,value代表客戶端連接的集合
* 作用:當節點發生變動時,直接獲取到所有監聽該節點的watcher,然後進行逐個通知
*/
private final HashMap<String, HashSet<Watcher>> watchTable =
    new HashMap<String, HashSet<Watcher>>();

/**
* key代表一個客戶端watcher,而value代表其監聽的所有節點路徑
* 爲啥要這樣弄勒?很簡單,當一個客戶端斷開了連接,那麼與之相關的一些監聽就也要去掉,也就是從第一個map中對應路徑的set中去掉這個watcher
* 所以這個map就是能快速定位到該watcher對應的所有path
*/
private final HashMap<Watcher, HashSet<String>> watch2Paths =
    new HashMap<Watcher, HashSet<String>>();

另外需要注意的是,服務端這裏的watcher並不是代碼裏面的那個Watcher,而只是一個實現了Watcher接口的ServerCnxn抽象類,

EventType註冊與通知流程

當客戶端調用對應的api(如getData等)發起註冊事件後,在客戶端會將該請求封裝成一個Packet對象,然後加入outGoingQueue隊列中等待發送。注意的是,此時還沒有將對應的worker維護進HashMap裏面,這一步需要等服務都安回調之後再進行

當服務端收到該對象之後,就會將watcher進行註冊,也就是更新一下那兩個HashMap,然後響應給客戶端。

客戶端接收到響應後,就會將對應的watcher註冊到自己的HashMap上。這樣,註冊就完成了

而通知過程也很簡單,服務端對對應節點進行修改,然後去掉自己的watchTable中找到所有的watcher,對它們進行一一通知,而客戶端的watcher的process函數則會被調用,完成一個通知的機制

需要注意的是,zookeeper中的事件通知是一次性的,也就是說,當服務端進行一次通知之後,就會把該watcher刪除掉(主要是處於性能考慮)

zookeeper中Watcher機制的特性

  • 一次性:無論是客戶端還是服務端,watcher一旦被觸發,那麼它就會被zk刪除,所以如果希望一直監聽的話,在每次回調之後還需要手動添加
  • 順序性。客戶端將收到的請求封裝爲Packet對象後,將其加入一個交outgoingQueue的FIFO隊列中,按照先來先服務的順序進行發送,從而保證了順序性
  • 輕量:對於服務端來說,它通知客戶端,並不通知事件的內容(比如具體節點內容進行了怎麼樣修改),它只告訴客戶端,發生了事件;而對於客戶端來說,它也並不會把整個watcher對象傳過去,只使用一個boolean來進行是否需要監聽嘛

簡單代碼示例

Talk is cheap嘛~

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

/**
 * Zookeeper Wathcher 
 * 本類就是一個Watcher類(實現了org.apache.zookeeper.Watcher類)
 * @author(alienware)
 * @since 2015-6-14
 */
public class ZooKeeperWatcher implements Watcher {
    /** 定義session失效時間 */
    private static final int SESSION_TIMEOUT = 5000;
    /** zookeeper服務器地址 */
    private static final String CONNECTION_ADDR = "ip1:port1,ip2:port2,ip3:port3";
    /** zk父路徑設置 */
    private static final String PARENT_PATH = "/a";
    /** zk子路徑設置 */
    private static final String CHILDREN_PATH = "/b/c";
    /** zk變量 */
    private ZooKeeper zk = null;

    /**
     * 創建ZK連接
     * @param connectAddr ZK服務器地址列表
     * @param sessionTimeout Session超時時間
     */
    public void createConnection(String connectAddr, int sessionTimeout) {
        this.releaseConnection();
        try {
            //this表示把當前對象進行傳遞到其中去(也就是在主函數裏實例化的new ZooKeeperWatcher()實例對象)
            zk = new ZooKeeper(connectAddr, sessionTimeout, this);
            System.out.println(LOG_PREFIX_OF_MAIN + "開始連接ZK服務器");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 關閉ZK連接
     */
    public void releaseConnection() {
        if (this.zk != null) {
            try {
                this.zk.close();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    
    /**
     * 收到來自Server的Watcher通知後的處理。
     */
    @Override
    public void process(WatchedEvent event) {
        
        System.out.println("進入process方法");
        
        if (event == null) {
            return;
        }
        
        // 獲取連接狀態
        KeeperState keeperState = event.getState();
        // 事件類型
        EventType eventType = event.getType();
        // 受影響的path
        String path = event.getPath();
        System.out.println("連接狀態:\t" + keeperState.toString());
        System.out.println("事件類型:\t" + eventType.toString());

        if (KeeperState.SyncConnected == keeperState) {
            // 成功連接上ZK服務器
            if (EventType.None == eventType) {
                System.out.println( "成功連接上ZK服務器");
                connectedSemaphore.countDown();
            } 
            //創建節點
            else if (EventType.NodeCreated == eventType) {
                System.out.println("節點創建");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } 
            //更新節點
            else if (EventType.NodeDataChanged == eventType) {
                System.out.println("節點數據更新");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } 
            //更新子節點
            else if (EventType.NodeChildrenChanged == eventType) {
                System.out.println("子節點變更");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } 
            //刪除節點
            else if (EventType.NodeDeleted == eventType) {
                System.out.println("節點 " + path + " 被刪除");
            }
            else ;
        } 
        else if (KeeperState.Disconnected == keeperState) {
            System.out.println("與ZK服務器斷開連接");
        } 
        else if (KeeperState.AuthFailed == keeperState) {
            System.out.println("權限檢查失敗");
        } 
        else if (KeeperState.Expired == keeperState) {
            System.out.println("會話失效");
        }

    }

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