【Zookeeper】原始碼分析之Watcher機制(三)之ZooKeeper

leesf發表於2017-01-18

一、前言

  前面已經分析了Watcher機制中的大多數類,本篇對於ZKWatchManager的外部類Zookeeper進行分析。

二、ZooKeeper原始碼分析

  2.1 類的內部類

  ZooKeeper的內部類框架圖如下圖所示

  

   說明:

  · ZKWatchManager,Zookeeper的Watcher管理者,其原始碼在之前已經分析過,不再累贅。

  · WatchRegistration,抽象類,用作watch註冊。

  · ExistsWatchRegistration,存在性watch註冊。

  · DataWatchRegistration,資料watch註冊。

  · ChildWatchRegistration,子節點註冊。

  · States,列舉型別,表示伺服器的狀態。

  1. WatchRegistration

  介面型別,表示對路徑註冊監聽。  

    abstract class WatchRegistration {
        // Watcher
        private Watcher watcher;
        // 客戶端路徑
        private String clientPath;
        
        // 建構函式
        public WatchRegistration(Watcher watcher, String clientPath)
        {
            this.watcher = watcher;
            this.clientPath = clientPath;
        }

        // 獲取路徑到Watchers集合的鍵值對,由子類實現
        abstract protected Map<String, Set<Watcher>> getWatches(int rc);

        /**
         * Register the watcher with the set of watches on path.
         * @param rc the result code of the operation that             attempted to
         * add the watch on the path.
         */
        // 註冊
        public void register(int rc) {
            if (shouldAddWatch(rc)) { // 應該新增監聽
                // 獲取路徑到Watchers集合的鍵值對,工廠模式
                Map<String, Set<Watcher>> watches = getWatches(rc);
                synchronized(watches) { // 同步塊
                    // 通過路徑獲取watcher集合
                    Set<Watcher> watchers = watches.get(clientPath);
                    if (watchers == null) { // watcher集合為空
                        // 新生成集合
                        watchers = new HashSet<Watcher>();
                        // 將路徑和watchers集合存入
                        watches.put(clientPath, watchers);
                    }
                    // 新增至watchers集合
                    watchers.add(watcher);
                }
            }
        }
        /**
         * Determine whether the watch should be added based on return code.
         * @param rc the result code of the operation that attempted to add the
         * watch on the node
         * @return true if the watch should be added, otw false
         */
        // 判斷是否需要新增,判斷rc是否為0
        protected boolean shouldAddWatch(int rc) {
            return rc == 0;
        }
    }

  說明:可以看到WatchRegistration包含了Watcher和clientPath欄位,表示監聽和對應的路徑,值得注意的是getWatches方式抽象方法,需要子類實現,而在register方法中會呼叫getWatches方法,實際上呼叫的是子類的getWatches方法,這是典型的工廠模式。register方法首先會判定是否需要新增監聽,然後再進行相應的操作,在WatchRegistration類的預設實現中shouldAddWatch是判定返回碼是否為0。

  2. ExistsWatchRegistration 

    class ExistsWatchRegistration extends WatchRegistration {
        // 建構函式
        public ExistsWatchRegistration(Watcher watcher, String clientPath) {
            // 呼叫父類建構函式
            super(watcher, clientPath);
        }
        
        @Override
        protected Map<String, Set<Watcher>> getWatches(int rc) {
            // 根據rc是否為0確定返回dataWatches或existsWatches
            return rc == 0 ?  watchManager.dataWatches : watchManager.existWatches;
        }

        @Override
        protected boolean shouldAddWatch(int rc) {
            // 判斷rc是否為0或者rc是否等於NONODE的值
            return rc == 0 || rc == KeeperException.Code.NONODE.intValue();
        }
    }

  說明:ExistsWatchRegistration 表示對存在性監聽的註冊,其實現了getWatches方法,並且重寫了shouldAddWatch方法,getWatches方法是根據返回碼的值確定返回dataWatches或者是existWatches。

  3. DataWatchRegistration 

    class DataWatchRegistration extends WatchRegistration {
        // 建構函式
        public DataWatchRegistration(Watcher watcher, String clientPath) {
            // 呼叫父類建構函式
            super(watcher, clientPath);
        }

        @Override
        protected Map<String, Set<Watcher>> getWatches(int rc) {
            // 直接返回dataWatches
            return watchManager.dataWatches;
        }
    }

  說明:DataWatchRegistration表示對資料監聽的註冊,其實現了getWatches方法,返回dataWatches。

  4. ChildWatchRegistration 

    class ChildWatchRegistration extends WatchRegistration {
        // 建構函式
        public ChildWatchRegistration(Watcher watcher, String clientPath) {
            // 呼叫父類建構函式
            super(watcher, clientPath);
        }

        @Override
        protected Map<String, Set<Watcher>> getWatches(int rc) {
            // 直接返回childWatches
            return watchManager.childWatches;
        }
    }

  說明:ChildWatchRegistration表示對子節點監聽的註冊,其實現了getWatches方法,返回childWatches。

  5. States

    public enum States {
        // 代表伺服器的狀態
        CONNECTING, ASSOCIATING, CONNECTED, CONNECTEDREADONLY,
        CLOSED, AUTH_FAILED, NOT_CONNECTED;

        // 是否存活
        public boolean isAlive() {
            // 不為關閉狀態並且未認證失敗
            return this != CLOSED && this != AUTH_FAILED;
        }

        /**
         * Returns whether we are connected to a server (which
         * could possibly be read-only, if this client is allowed
         * to go to read-only mode)
         * */
        // 是否連線
        public boolean isConnected() {
            // 已連線或者只讀連線
            return this == CONNECTED || this == CONNECTEDREADONLY;
        }
    }

  說明:States為列舉類,表示伺服器的狀態,其有兩個方法,判斷伺服器是否存活和判斷客戶端是否連線至服務端。

  2.2 類的屬性  

public class ZooKeeper {
    // 客戶端Socket
    public static final String ZOOKEEPER_CLIENT_CNXN_SOCKET = "zookeeper.clientCnxnSocket";
    
    // 客戶端,用來管理客戶端與服務端的連線
    protected final ClientCnxn cnxn;
    
    // Logger日誌
    private static final Logger LOG;
    static {
        //Keep these two lines together to keep the initialization order explicit
        // 初始化
        LOG = LoggerFactory.getLogger(ZooKeeper.class);
        Environment.logEnv("Client environment:", LOG);
    }
  private final ZKWatchManager watchManager = new ZKWatchManager();
}

  說明:ZooKeeper類存維護一個ClientCnxn類,用來管理客戶端與服務端的連線。  

  2.3 類的建構函式

  1. ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, boolean canBeReadOnly)型建構函式    

    public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
            boolean canBeReadOnly)
        throws IOException
    {
        LOG.info("Initiating client connection, connectString=" + connectString
                + " sessionTimeout=" + sessionTimeout + " watcher=" + watcher);
        // 初始化預設Watcher
        watchManager.defaultWatcher = watcher;

        // 對傳入的connectString進行解析
        // connectString 類似於127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002未指定根空間的字串
        // 或者是127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a指定根空間的字串,根為/app/a
        ConnectStringParser connectStringParser = new ConnectStringParser(
                connectString);
                
        // 根據伺服器地址列表生成HostProvider
        HostProvider hostProvider = new StaticHostProvider(
                connectStringParser.getServerAddresses());
        // 生成客戶端管理
        cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
                hostProvider, sessionTimeout, this, watchManager,
                getClientCnxnSocket(), canBeReadOnly);
        // 啟動
        cnxn.start();
    }

  說明:該建構函式會初始化WatchManager的defaultWatcher,同時會解析服務端地址和埠號,之後根據服務端的地址生成HostProvider(其會打亂伺服器的地址),之後生成客戶端管理並啟動,注意此時會呼叫getClientCnxnSocket函式,其原始碼如下  

    private static ClientCnxnSocket getClientCnxnSocket() throws IOException {
        // 檢視是否在系統屬性中進行了設定
        String clientCnxnSocketName = System
                .getProperty(ZOOKEEPER_CLIENT_CNXN_SOCKET);
        if (clientCnxnSocketName == null) { // 若未進行設定,取得ClientCnxnSocketNIO的類名
            clientCnxnSocketName = ClientCnxnSocketNIO.class.getName();
        }
        try {
            // 使用反射新生成例項然後返回
            return (ClientCnxnSocket) Class.forName(clientCnxnSocketName)
                    .newInstance();
        } catch (Exception e) {
            IOException ioe = new IOException("Couldn't instantiate "
                    + clientCnxnSocketName);
            ioe.initCause(e);
            throw ioe;
        }
    }

  說明:該函式會利用反射建立ClientCnxnSocketNIO例項

  2. public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd, boolean canBeReadOnly) throws IOException型建構函式  

    public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
            long sessionId, byte[] sessionPasswd, boolean canBeReadOnly)
        throws IOException
    {
        LOG.info("Initiating client connection, connectString=" + connectString
                + " sessionTimeout=" + sessionTimeout
                + " watcher=" + watcher
                + " sessionId=" + Long.toHexString(sessionId)
                + " sessionPasswd="
                + (sessionPasswd == null ? "<null>" : "<hidden>"));

        // 初始化預設Watcher
        watchManager.defaultWatcher = watcher;

        // 對傳入的connectString進行解析
        // connectString 類似於127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002未指定根空間的字串
        // 或者是127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a指定根空間的字串,根為/app/a
        ConnectStringParser connectStringParser = new ConnectStringParser(
                connectString);
                
        // 根據伺服器地址列表生成HostProvider
        HostProvider hostProvider = new StaticHostProvider(
                connectStringParser.getServerAddresses());
        // 生成客戶端時使用了session密碼
        cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
                hostProvider, sessionTimeout, this, watchManager,
                getClientCnxnSocket(), sessionId, sessionPasswd, canBeReadOnly);
                
        // 設定客戶端的seenRwServerBefore欄位為true(因為使用者提供了sessionId,表示肯定已經連線過)
        cnxn.seenRwServerBefore = true; // since user has provided sessionId
        // 啟動
        cnxn.start();
    }

  說明:此型建構函式和之前建構函式的區別在於本建構函式提供了sessionId和sessionPwd,這表明使用者已經之前已經連線過服務端,所以能夠獲取到sessionId,其流程與之前的建構函式類似,不再累贅。

  2.4 核心函式分析

  1. create函式  

  函式簽名:public String create(final String path, byte data[], List<ACL> acl, CreateMode createMode) throws KeeperException, InterruptedException

    public String create(final String path, byte data[], List<ACL> acl,
            CreateMode createMode)
        throws KeeperException, InterruptedException
    {
        final String clientPath = path;
        
        // 驗證路徑是否合法
        PathUtils.validatePath(clientPath, createMode.isSequential());

        // 新增根空間
        final String serverPath = prependChroot(clientPath);

        // 新生請求頭
        RequestHeader h = new RequestHeader();
        // 設定請求頭型別
        h.setType(ZooDefs.OpCode.create);
        // 新生建立節點請求
        CreateRequest request = new CreateRequest();
        // 新生建立節點響應
        CreateResponse response = new CreateResponse();
        // 設定請求的資料
        request.setData(data);
        // 設定請求對應的Flag
        request.setFlags(createMode.toFlag());
        // 設定伺服器路徑
        request.setPath(serverPath);
        if (acl != null && acl.size() == 0) { // ACL不為空但是大小為0,丟擲異常
            throw new KeeperException.InvalidACLException();
        }
        // 設定請求的ACL列表
        request.setAcl(acl);
        // 提交請求
        ReplyHeader r = cnxn.submitRequest(h, request, response, null);
        if (r.getErr() != 0) { // 請求的響應的錯誤碼不為0,則丟擲異常
            throw KeeperException.create(KeeperException.Code.get(r.getErr()),
                    clientPath);
        }
        if (cnxn.chrootPath == null) { // 根空間為空
            // 則返回響應中的路徑
            return response.getPath();
        } else {
            // 除去根空間後返回
            return response.getPath().substring(cnxn.chrootPath.length());
        }
    }

  說明:該create函式是同步的,主要用作建立節點,其大致步驟如下

  ① 驗證路徑是否合法,若不合法,丟擲異常,否則進入②

  ② 新增根空間,生成請求頭、請求、響應等,並設定相應欄位,進入③

  ③ 通過客戶端提交請求,判斷返回碼是否為0,若不是,則丟擲異常,否則,進入④

  ④ 除去根空間後,返回響應的路徑

  其中會呼叫submitRequest方法,其原始碼如下  

    public ReplyHeader submitRequest(RequestHeader h, Record request,
            Record response, WatchRegistration watchRegistration)
            throws InterruptedException {
        // 新生響應頭
        ReplyHeader r = new ReplyHeader();
        // 新生Packet包
        Packet packet = queuePacket(h, r, request, response, null, null, null,
                    null, watchRegistration);
        synchronized (packet) { // 同步
            while (!packet.finished) { // 如果沒有結束
                // 則等待
                packet.wait();
            }
        }
        // 返回響應頭
        return r;
    }

  說明:submitRequest會將請求封裝成Packet包,然後一直等待packet包響應結束,然後返回;若沒結束,則等待。可以看到其是一個同步方法。

  2. create函式

  函式簽名:public void create(final String path, byte data[], List<ACL> acl, CreateMode createMode, StringCallback cb, Object ctx)  

    public void create(final String path, byte data[], List<ACL> acl,
            CreateMode createMode,  StringCallback cb, Object ctx)
    {
        final String clientPath = path;
        
        // 驗證路徑是否合法
        PathUtils.validatePath(clientPath, createMode.isSequential());

        // 新增根空間
        final String serverPath = prependChroot(clientPath);

        // 新生請求頭
        RequestHeader h = new RequestHeader();
        // 設定請求頭型別
        h.setType(ZooDefs.OpCode.create);
        // 新生建立節點請求
        CreateRequest request = new CreateRequest();
        // 新生建立節點響應
        CreateResponse response = new CreateResponse();
        // 新生響應頭
        ReplyHeader r = new ReplyHeader();
        // 設定請求的資料
        request.setData(data);
        // 設定請求對應的Flag
        request.setFlags(createMode.toFlag());
        // 設定服務
        request.setPath(serverPath);
        // 設定ACL列表
        request.setAcl(acl);
        // 封裝成packet放入佇列,等待提交
        cnxn.queuePacket(h, r, request, response, cb, clientPath,
                serverPath, ctx, null);
    }

  說明:該create函式是非同步的,其大致步驟與同步版的create函式相同,只是最後其會將請求打包成packet,然後放入佇列等待提交。

  3. delete函式  

  函式簽名:public void delete(final String path, int version) throws InterruptedException, KeeperException

    public void delete(final String path, int version)
        throws InterruptedException, KeeperException
    {
        final String clientPath = path;
        // 驗證路徑的合法性
        PathUtils.validatePath(clientPath);

        final String serverPath;

        // maintain semantics even in chroot case
        // specifically - root cannot be deleted
        // I think this makes sense even in chroot case.
        if (clientPath.equals("/")) { // 判斷是否是"/",即zookeeper的根目錄,根目錄無法刪除
            // a bit of a hack, but delete(/) will never succeed and ensures
            // that the same semantics are maintained
            // 
            serverPath = clientPath;
        } else { // 新增根空間
            serverPath = prependChroot(clientPath);
        }
        
        // 新生請求頭
        RequestHeader h = new RequestHeader();
        // 設定請求頭型別
        h.setType(ZooDefs.OpCode.delete);
        // 新生刪除請求
        DeleteRequest request = new DeleteRequest();
        // 設定路徑
        request.setPath(serverPath);
        // 設定版本號
        request.setVersion(version);
        // 新生響應頭
        ReplyHeader r = cnxn.submitRequest(h, request, null, null);
        if (r.getErr() != 0) { // 判斷返回碼
            throw KeeperException.create(KeeperException.Code.get(r.getErr()),
                    clientPath);
        }
    }

  說明:該函式是同步的,其流程與create流程相似,不再累贅。

  4. delete函式

  函式簽名:public void delete(final String path, int version, VoidCallback cb, Object ctx)

    public void delete(final String path, int version, VoidCallback cb,
            Object ctx)
    {
        final String clientPath = path;
        
        // 驗證路徑是否合法
        PathUtils.validatePath(clientPath);

        final String serverPath;

        // maintain semantics even in chroot case
        // specifically - root cannot be deleted
        // I think this makes sense even in chroot case.
        if (clientPath.equals("/")) { // 判斷是否是"/",即zookeeper的根目錄,根目錄無法刪除
            // a bit of a hack, but delete(/) will never succeed and ensures
            // that the same semantics are maintained
            serverPath = clientPath;
        } else {
            serverPath = prependChroot(clientPath);
        }
        
        // 新生請求頭
        RequestHeader h = new RequestHeader();
        // 設定請求頭型別
        h.setType(ZooDefs.OpCode.delete);
        // 新生刪除請求
        DeleteRequest request = new DeleteRequest();
        // 設定路徑
        request.setPath(serverPath);
        // 設定版本號
        request.setVersion(version);
        // 封裝成packet放入佇列,等待提交
        cnxn.queuePacket(h, new ReplyHeader(), request, null, cb, clientPath,
                serverPath, ctx, null);
    }

  說明:該函式是非同步的,其流程也相對簡單,不再累贅。

  5. multi函式  

    public List<OpResult> multi(Iterable<Op> ops) throws InterruptedException, KeeperException {
        for (Op op : ops) { // 驗證每個操作是否合法
            op.validate();
        }
        // reconstructing transaction with the chroot prefix
        // 新生事務列表
        List<Op> transaction = new ArrayList<Op>();
        for (Op op : ops) { // 將每個操作新增根空間後新增到事務列表中
            transaction.add(withRootPrefix(op));
        }
        // 呼叫multiInternal後返回
        return multiInternal(new MultiTransactionRecord(transaction));
    }

  說明:該函式用於執行多個操作或者不執行,其首先會驗證每個操作的合法性,然後將每個操作新增根空間後加入到事務列表中,之後會呼叫multiInternal函式,其原始碼如下  

    protected List<OpResult> multiInternal(MultiTransactionRecord request)
        throws InterruptedException, KeeperException {
        // 新生請求頭
        RequestHeader h = new RequestHeader();
        // 設定請求頭型別
        h.setType(ZooDefs.OpCode.multi);
        // 新生多重響應
        MultiResponse response = new MultiResponse();
        // 新生響應頭
        ReplyHeader r = cnxn.submitRequest(h, request, response, null);
        if (r.getErr() != 0) { // 判斷返回碼是否為0
            throw KeeperException.create(KeeperException.Code.get(r.getErr()));
        }

        // 獲取響應的結果集
        List<OpResult> results = response.getResultList();
        
        ErrorResult fatalError = null;
        for (OpResult result : results) { // 遍歷結果集
            if (result instanceof ErrorResult && ((ErrorResult)result).getErr() != KeeperException.Code.OK.intValue()) { //判斷結果集中是否出現了異常
                fatalError = (ErrorResult) result;
                break;
            }
        }

        if (fatalError != null) { // 出現了異常
            // 新生異常後丟擲
            KeeperException ex = KeeperException.create(KeeperException.Code.get(fatalError.getErr()));
            ex.setMultiResults(results);
            throw ex;
        }

        // 返回結果集
        return results;
    }

  說明:multiInternal函式會提交多個操作並且等待響應結果集,然後判斷結果集中是否有異常,若有異常則丟擲異常,否則返回響應結果集。

  6. exists函式  

  函式簽名:public Stat exists(final String path, Watcher watcher) throws KeeperException, InterruptedException

    public Stat exists(final String path, Watcher watcher)
        throws KeeperException, InterruptedException
    {
        final String clientPath = path;
        
        // 驗證路徑是否合法
        PathUtils.validatePath(clientPath);

        // the watch contains the un-chroot path
        WatchRegistration wcb = null;
        if (watcher != null) { // 生成存在性註冊
            wcb = new ExistsWatchRegistration(watcher, clientPath);
        }

        // 新增根空間
        final String serverPath = prependChroot(clientPath);

        // 新生請求頭
        RequestHeader h = new RequestHeader();
        // 設定請求頭型別
        h.setType(ZooDefs.OpCode.exists);
        // 新生節點存在請求
        ExistsRequest request = new ExistsRequest();
        // 設定路徑
        request.setPath(serverPath);
        // 設定Watcher
        request.setWatch(watcher != null);
        // 新生設定資料響應
        SetDataResponse response = new SetDataResponse();
        // 提交請求
        ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
        if (r.getErr() != 0) { // 判斷返回碼
            if (r.getErr() == KeeperException.Code.NONODE.intValue()) {
                return null;
            }
            throw KeeperException.create(KeeperException.Code.get(r.getErr()),
                    clientPath);
        }
        
        // 返回結果的狀態
        return response.getStat().getCzxid() == -1 ? null : response.getStat();
    }

  說明:該函式是同步的,用於判斷指定路徑的節點是否存在,值得注意的是,其會對指定路徑的結點進行註冊監聽。

  7. exists

  函式簽名:public void exists(final String path, Watcher watcher, StatCallback cb, Object ctx) 

    public void exists(final String path, Watcher watcher,
            StatCallback cb, Object ctx)
    {
        final String clientPath = path;
        // 驗證路徑是否合法
        PathUtils.validatePath(clientPath);

        // the watch contains the un-chroot path
        WatchRegistration wcb = null;
        if (watcher != null) { // 生成存在性註冊
            wcb = new ExistsWatchRegistration(watcher, clientPath);
        }

        // 新增根空間
        final String serverPath = prependChroot(clientPath);
        // 新生請求頭
        RequestHeader h = new RequestHeader();
        // 設定請求頭型別
        h.setType(ZooDefs.OpCode.exists);
        // 新生節點存在請求
        ExistsRequest request = new ExistsRequest();
        // 設定路徑
        request.setPath(serverPath);
        // 設定Watcher
        request.setWatch(watcher != null);
        // 新生設定資料響應
        SetDataResponse response = new SetDataResponse();
        // 將請求封裝成packet,放入佇列,等待執行
        cnxn.queuePacket(h, new ReplyHeader(), request, response, cb,
                clientPath, serverPath, ctx, wcb);
    }

  說明:該函式是非同步的,與同步的流程相似,不再累贅。

  之後的getData、setData、getACL、setACL、getChildren函式均類似,只是生成的響應類別和監聽類別不相同,大同小異,不再累贅。

三、總結

  本篇博文分析了Watcher機制的ZooKeeper類,該類包括了對伺服器的很多事務性操作,並且包含了同步和非同步兩個版本,但是相對來說,較為簡單,也謝謝各位園友的觀看~ 

相關文章