相關類
Watcher 接口
任何一個事件處理類都必須實現Watcher
接口,它有一個內部接口 Event
,以及 process
方法。前者定義了ZK的狀態和事件類型的枚舉,後者則定義了事件的回調方法。
@InterfaceAudience.Public
public interface Watcher {
@InterfaceAudience.Public
public interface Event {
@InterfaceAudience.Public
public enum KeeperState {/*...*/}
@InterfaceAudience.Public
public enum EventType {/*...*/}
}
abstract public void process(WatchedEvent event);
}
回調方法 process
只有一個參數 WatchedEvent
,這個類也很簡單,只有三個參數,分別是通知狀態、事件類型,以及節點路徑。
public class WatchedEvent {
final private KeeperState keeperState;
final private EventType eventType;
private String path;
/*...*/
}
和 WatchedEvent
緊密相關的還有 WatcherEvent
,兩者都是對ZK服務端事件的封裝,不過後者實現了序列化接口,可用於網絡傳輸。
ZK服務端會將 WatchedEvent
包裝成 WatcherEvent
進行傳輸,客戶端則需逆向處理,解包裝成WatchedEvent
來處理事件。
可以看到,WatchedEvent
和 WatcherEvent
都只有簡單的事件本身的信息,而不包含具體的內容,因此需要客戶端主動去獲取感興趣的最新數據。
工作機制
客戶端註冊
客戶端可以通過 getData()
、getChildren()
和exist()
三個接口來向服務端註冊 Watcher。註冊的原理都是相同的,這裏僅以 getData()
爲例進行分析。
getData
有兩個重載方法,區別在於第二個參數。
// 使用自定義的 watcher
public byte[] getData(final String path, Watcher watcher, Stat stat)
// 是否使用默認的 watcher
public byte[] getData(String path, boolean watch, Stat stat)
默認的 watcher 在實例化 ZooKeeper
的時候指定:
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
ZooKeeper
實例維護了一個 ZKWatchManager
類的實例,該類實現了ClientWatchManager
接口,是客戶端的 watcher 管理器。
默認 watcher 就保存在 ZKWatchManager
的 defaultWatcher
中。
private final ZKWatchManager watchManager = new ZKWatchManager();
private static class ZKWatchManager implements ClientWatchManager {
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>>();
private volatile Watcher defaultWatcher;
/* ... */
}
回到getData
,傳遞 watcher 參數後,首先會被封裝成 WatchRegistration
對象,並設置 request 對象爲“使用watcher監聽”:
public byte[] getData(final String path, Watcher watcher, Stat stat)
throws KeeperException, InterruptedException {
/* ... */
WatchRegistration wcb = null;
if (watcher != null) {
wcb = new DataWatchRegistration(watcher, clientPath);
}
/* ... */
request.setWatch(watcher != null);
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
/* ... */
}
然後在客戶端連接對象cnxn(ClientCnxn
)的 submitRequest()
方法中,又被封裝成 Packet
對象進行網絡傳輸。
(PS:Packet
在ZK中可以被看作最小的通信協議單元)
public ReplyHeader submitRequest(RequestHeader h, Record request,
Record response, WatchRegistration watchRegistration)
throws InterruptedException {
/* ... */
Packet packet = queuePacket(h, r, request, response, null, null, null,
null, watchRegistration);
/* ... */
}
發送完請求後,由ClientCnxn
的 SendThread
線程的 readResponse()
方法負責接收服務端的響應。
void readResponse(ByteBuffer incomingBuffer) throws IOException {
/* ... */
finishPacket(packet);
}
在 finishPacket()
中註冊 watcher 到 ZKWatchManager
的 dataWatches
中。
private void finishPacket(Packet p) {
// 註冊 watcher
// p.replyHeader.getErr() 爲0,則代表服務端響應成功。
if (p.watchRegistration != null) {
p.watchRegistration.register(p.replyHeader.getErr());
}
/* ... */
}
ZooKeeper.java
public void register(int rc) {
if (shouldAddWatch(rc)) {
// 獲取已有的 watcher 映射列表,若無則新增
Map<String, Set<Watcher>> watches = getWatches(rc);
synchronized(watches) {
Set<Watcher> watchers = watches.get(clientPath);
if (watchers == null) {
watchers = new HashSet<Watcher>();
watches.put(clientPath, watchers);
}
watchers.add(watcher);
}
}
}
dataWatches
是一個 Map<String, Set<Watcher>>
,保存了節點路徑和watcher的映射關係。
到此,客戶端註冊 Watcher 完畢。稍微總結一下:
- 調用客戶端API,傳入 watcher;
- 標記 request,封裝 watcher 到 WatcherRegistration;
- 向服務端發送 request;
- 若響應成功,則註冊 watcher 到 ZKWatcherManager 中進行管理;
值得一提的是,packet 對象在序列化時,並沒有把 watcher 對象也一併序列化,以此降低網絡傳輸的成本,以及服務端的內存壓力。
服務端處理
從客戶端接收到請求後,服務端會在 FinalRequestProcessor.processRequest()
方法中判斷是否需要註冊 watcher:
public void processRequest(Request request) {
/* ... */
switch (request.type) {
/* ... */
case OpCode.getData: {
/* ... */
byte b[] = zks.getZKDatabase().getData(getDataRequest.getPath(), stat,
getDataRequest.getWatch() ? cnxn : null);
rsp = new GetDataResponse(b, stat);
break;
}
}
/* ... */
}
可以看到,當 getDataRequest.getWatch()
爲 true 時(也就是在客戶端標記的),就會將當前的 ServerCnxn
對象傳入 ZKDatabase.getData()
方法中。
在這裏 ServerCnxn
是 Watcher
接口的實現類,可以看做一個 Watcher
:
public abstract class ServerCnxn implements Stats, Watcher {
/* ...*/
}
ZKDatabase.getData()
這個類維護了 ZK 的內存數據庫,包括Session、DataTree和committed logs
public byte[] getData(String path, Stat stat, Watcher watcher)
throws KeeperException.NoNodeException {
return dataTree.getData(path, stat, watcher);
}
DataTree.getData()
public byte[] getData(String path, Stat stat, Watcher watcher)
throws KeeperException.NoNodeException {
DataNode n = nodes.get(path);
if (n == null) {
throw new KeeperException.NoNodeException();
}
synchronized (n) {
n.copyStat(stat);
// 在這裏將 watcher 添加到了 WatcherManager 的 dataWatches 中了
if (watcher != null) {
dataWatches.addWatch(path, watcher);
}
return n.data;
}
}
WatchManager.addWatch()
WatchManager 從兩個維度來保存 watcher
public synchronized void addWatch(String path, Watcher watcher) {
HashSet<Watcher> list = watchTable.get(path);
if (list == null) {
// don't waste memory if there are few watches on a node
// rehash when the 4th entry is added, doubling size thereafter
// seems like a good compromise
list = new HashSet<Watcher>(4);
watchTable.put(path, list);
}
list.add(watcher);
HashSet<String> paths = watch2Paths.get(watcher);
if (paths == null) {
// cnxns typically have many watches, so use default cap here
paths = new HashSet<String>();
watch2Paths.put(watcher, paths);
}
paths.add(path);
}
到這裏就完成了服務端的watcher註冊和存儲了。
再來看看是怎麼觸發的。
前面是註冊了監聽某節點數據變動的watcher,所以當在該節點修改數據時,會觸發NodeDataChanged
事件:
DataTree.setData()
public Stat setData(String path, byte data[], int version, long zxid,
long time) throws KeeperException.NoNodeException {
Stat s = new Stat();
DataNode n = nodes.get(path);
if (n == null) {
throw new KeeperException.NoNodeException();
}
byte lastdata[] = null;
synchronized (n) {
lastdata = n.data;
n.data = data;
n.stat.setMtime(time);
n.stat.setMzxid(zxid);
n.stat.setVersion(version);
n.copyStat(s);
}
// now update if the path is in a quota subtree.
String lastPrefix;
if((lastPrefix = getMaxPrefixWithQuota(path)) != null) {
this.updateBytes(lastPrefix, (data == null ? 0 : data.length)
- (lastdata == null ? 0 : lastdata.length));
}
// 在這裏, 由 WatcherManager 觸發
dataWatches.triggerWatch(path, EventType.NodeDataChanged);
return s;
}
WatchManager.triggerWatch()
public Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
// 封裝一個 WatchedEvent 對象
WatchedEvent e = new WatchedEvent(type,
KeeperState.SyncConnected, path);
HashSet<Watcher> watchers;
synchronized (this) {
watchers = watchTable.remove(path);
if (watchers == null || watchers.isEmpty()) {
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG,
ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"No watchers for " + path);
}
return null;
}
for (Watcher w : watchers) {
HashSet<String> paths = watch2Paths.get(w);
if (paths != null) {
paths.remove(path);
}
}
}
for (Watcher w : watchers) {
if (supress != null && supress.contains(w)) {
continue;
}
// 這裏執行 ServerCnxn 的 process() 方法
w.process(e);
}
return watchers;
}
ServerCnxn.process()
@Override
synchronized public void process(WatchedEvent event) {
// 請求頭的xid(第一個參數)爲“-1”,代表是一個通知
ReplyHeader h = new ReplyHeader(-1, -1L, 0);
/* ... */
// 前面說過,要將 WatchedEvent 包裝成 WatcherEvent
WatcherEvent e = event.getWrapper();
sendResponse(h, e, "notification");
}
可以看到,服務端觸發 watcher 的邏輯是比較簡單的。
客戶端回調
在客戶端,由SendThread.readResponse()
處理服務端的的響應:
void readResponse(ByteBuffer incomingBuffer) throws IOException {
/* ... */
// -1 ,代表是個通知
if (replyHdr.getXid() == -1) {
WatcherEvent event = new WatcherEvent();
// 反序列化
event.deserialize(bbia, "response");
// ChrootPath 處理
if (chrootPath != null) {
String serverPath = event.getPath();
if(serverPath.compareTo(chrootPath)==0)
event.setPath("/");
else if (serverPath.length() > chrootPath.length())
event.setPath(serverPath.substring(chrootPath.length()));
else {
LOG.warn("Got server path " + event.getPath()
+ " which is too short for chroot path "
+ chrootPath);
}
}
// 這裏將 WatcherEvent 還原
WatchedEvent we = new WatchedEvent(event);
// 交給 EventThread,在下個輪詢週期回調
eventThread.queueEvent( we );
return;
}
/* ... */
}
EventThread
是ZK中專門用來處理服務端通知事件的線程:
public void queueEvent(WatchedEvent event) {
if (event.getType() == EventType.None
&& sessionState == event.getState()) {
return;
}
sessionState = event.getState();
// 根據事件的各種屬性,取出所有watcher
WatcherSetEventPair pair = new WatcherSetEventPair(
watcher.materialize(event.getState(), event.getType(),
event.getPath()),
event);
// queue the pair (watch set & event) for later processing
waitingEvents.add(pair);
}
客戶端識別事件的類型,從響應的Watcher存儲中去除watcher,並加到結果中:
public Set<Watcher> materialize(Watcher.Event.KeeperState state,
Watcher.Event.EventType type,
String clientPath)
{
Set<Watcher> result = new HashSet<Watcher>();
switch (type) {
/* ... */
case NodeDataChanged:
case NodeCreated:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
synchronized (existWatches) {
addTo(existWatches.remove(clientPath), result);
}
break;
/* ... */
}
return result;
}
在 EventThread
的 run()
方法中,將會不斷地處理 waitingEvents 隊列中的watcher。
小結
由此,便完成了Watcher機制源碼的簡要分析,從中可以發現Watcher的幾個特性:
- 一次性,無論是客戶端還是服務端,一旦觸發一個watcher,就將被移除;
- 客戶端串行執行,所有的watcher回調都是在一個隊列中串行執行的,要注意不要因爲一個watcher的處理邏輯影響了整體的回調;
- 輕量,服務端不會講事件的具體內容告知客戶端,客戶端註冊watcher的時候也不會發送真實的watcher對象。