一、前言
前面已經分析了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類,該類包括了對伺服器的很多事務性操作,並且包含了同步和非同步兩個版本,但是相對來說,較為簡單,也謝謝各位園友的觀看~