前言
- zookeeper 为啥要引入watcher机制?
- watcher机制解决了什么样的问题?
- watcher机制使用的场景在哪里?
- watcher机制的实现原理是什么?
zk为啥要引入watcher机制
- 在集群中,有很多机器,当某个机器中的配置发生变化后,如何让所有的集群配置统一修改,保证集群数据的一致性?
- 集群中某个节点宕机,如何让集群中的其他节点知道?
因此这时候zk就提供了watcher机制,保证了数据的一致性,当数据发生改变时,触发客户端进行改变。
watcher机制
- 客户端watcher注册
- 服务端处理watcher
- 客户端回调watcher事件
客户端watcher注册
那客户端如何注册watcher?
zookeeper 在读取操作时候提供了watcher注册功能,保证集群其他节点数据发生改变时,所有的集群机器都能够感知,并发生改变,保证数据的一致性。
对于 getData(),getChildren()和exists()被调用时,客户端可以自己决定是否需要注册watcher事件。
先看下这三个对应注册监听的方法源码
getData源码中,会判断客户端是否需要监听事件,如果有监听事件不为空,就将该监听事件放在
DataWatchRegistration 数据监听注册对象里,将监听事件(watcher)和节点路径(path)对应起来。
if (watcher != null) {
wcb = new ZooKeeper.DataWatchRegistration(watcher, path);
}
getChildren方法中,跟getData类似,只是将对象放在ChildWatchRegistration监听注册对象里。
if (watcher != null) {
wcb = new ZooKeeper.ChildWatchRegistration(watcher, path);
}
和exists方法里也是如此,当客户端调用和exists方法时候,会将客户端注册的事件放在
ExistsWatchRegistration监听注册对象里
if (watcher != null) {
wcb = new ZooKeeper.ExistsWatchRegistration(watcher, path);
}
(1)在具体看下这三个对象DataWatchRegistration,ChildWatchRegistration,
ExistsWatchRegistration,发现这三个对象都继承了相同的父类WatchRegistration。
先在看下这抽象类WatchRegistration,具体提供了什么样操作?
abstract class WatchRegistration {
// 声明了两个属性,监听和路径,将客户端的事件监听和节点路径对应起来
private Watcher watcher;
private String clientPath;
// 构造函数
public WatchRegistration(Watcher watcher, String clientPath) {
this.watcher = watcher;
this.clientPath = clientPath;
}
// 抽象方法,获取客户端监听列表,由不同的子类自己实现(不同的监听类型获取不同的事件监听列表)
protected abstract Map<String, Set<Watcher>> getWatches(int var1);
// 父类统一实现watcher事件注册方法
public void register(int rc) {
if (this.shouldAddWatch(rc)) {
// 获取监听列表(子类实现getWatches()方法)
Map<String, Set<Watcher>> watches = this.getWatches(rc);
synchronized(watches) {
// 根据节点路径获取对应的监听事件
Set<Watcher> watchers = (Set)watches.get(this.clientPath);
// 同一个节点的监听事件放在同一个集合里
if (watchers == null) {
watchers = new HashSet();
watches.put(this.clientPath, watchers);
}
((Set)watchers).add(this.watcher);
}
}
}
// rc ? rc=== returnCode 表示的是从服务端返回的响应代码
// 如果响应成功后,可以将该次的监听放入在监听列表里
protected boolean shouldAddWatch(int rc) {
return rc == 0;
}
}
通过源码知道WatchRegistration提供了两个属性,wathcer和path,当客户端注册事件时,构造函数将注册事件和节点路径对应起来,当某个节点增加监听对象时,在把该监听对象放在节点对应的监听集合里。
WatchRegistration类中重要的是watcher事件注册方法,子类通过实现父类的getWatches方法,获取到子类对应的watcher列表,然后通过节点路径获取到该节点路径对应的watche集合,之后添加watcher事件。
(2)看完父类WatchRegistration的源码后,我们在看下子类的实现,举例下DataWatchRegistration
class DataWatchRegistration extends ZooKeeper.WatchRegistration {
// 调用父类的构造方法,绑定watcher和path
public DataWatchRegistration(Watcher watcher, String clientPath) {
super(watcher, clientPath);
}
// 实现父类的getWatches方法
protected Map<String, Set<Watcher>> getWatches(int rc) {
return ZooKeeper.this.watchManager.dataWatches;
}
}
在这个DataWatchRegistration子类中,getWatches实现逻辑是通过ZooKeeper.this.watchManager.dataWatches获取对应的监听列表。
(3)那对于客户端获取监听列表时,监听管理者是何时加载监听列表的?
还记得在客户端在作读取操作时候,还作了什么操作吗?没错?总要去初始化Zookeeper对象,那我们接下来看下new Zookeeper对象作了什么操作?
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, boolean canBeReadOnly) throws IOException {
// 初始化监听管理者对象,初始化三个监听列表对象
this.watchManager = new ZooKeeper.ZKWatchManager((ZooKeeper.SyntheticClass_1)null);
LOG.info("Initiating client connection, connectString=" + connectString + " sessionTimeout=" + sessionTimeout + " watcher=" + watcher);
// zk初始化时,可允许用户注册监听事件。来监听客户端与服务器端的连接状态,这个默认监听是一直在的。
// 默认监听对象也放在监听管理者对象里
this.watchManager.defaultWatcher = watcher;
// connectString参数是zk服务器的地址 将服务地址放在serverAddresses地址里
ConnectStringParser connectStringParser = new ConnectStringParser(connectString);
// 封装跟服务端交互相关的信息
HostProvider hostProvider = new StaticHostProvider(connectStringParser.getServerAddresses());
//zk初始化时,new了一个ClientCnxn,这个类主要是作跟服务端作交互,发送数据,监听回调都会在这个类里
this.cnxn = new ClientCnxn(connectStringParser.getChrootPath(), hostProvider, sessionTimeout, this, this.watchManager, getClientCnxnSocket(), canBeReadOnly);
// 启动客户端,初始化客户端后,客户端会启动两个线程,一个是sendThread线程,发送消息给服务端线程,一个是e // ventThread线程,事件线程,当客户端有注册监听事件时,服务端触发客户端作回调操作时,都是通过该线程来处理。
this.cnxn.start();
}
看到zk初始化时候,客户端做了很多操作,比如说初始化监听管理者对象,注册默认监听事件,初始化ClientCnxn对象,连接服务器等。
(4)那这个ZKWatchManager,是在哪里进行设值监听列表的?
我们先了解下这个ZKWatchManager对象,
private static class ZKWatchManager implements ClientWatchManager {
private final Map<String, Set<Watcher>> dataWatches;
private final Map<String, Set<Watcher>> existWatches;
private final Map<String, Set<Watcher>> childWatches;
private volatile Watcher defaultWatcher;
private ZKWatchManager() {
this.dataWatches = new HashMap();
this.existWatches = new HashMap();
this.childWatches = new HashMap();
}
private final void addTo(Set<Watcher> from, Set<Watcher> to) {
if (from != null) {
to.addAll(from);
}
}
public Set<Watcher> materialize(KeeperState state, EventType type, String clientPath) {
// 省略代码
}
}
可知该对象里有四个属性,有一个是默认监听事件,该监听事件是一直存在的。而其他三次监听事件对应不同读取操作类型,
比如说getData()对应的是dataWatches,getChildren()对应的是childWatches,exists()对应的是existWatches。
还提供了两个方法,addTo()和materialize(),重点主要是materialize(),该方法主要做的是,服务器响应回来后,客户端回调注册事件,解析注册事件的逻辑处理,暂不展开。
知道了该类主要是客户端管理不同注册事件类型集合的对象,方便客户端使用,知道了该类,我们继续往下看下ClientCnxn类,看下该类具体做了什么操作?刚才初始化zk对象时候,调用了ClientCnxn的构造函数,我们看下具体的实现代码
public ClientCnxn(String chrootPath, HostProvider hostProvider, int sessionTimeout, ZooKeeper zooKeeper, ClientWatchManager watcher, ClientCnxnSocket clientCnxnSocket, long sessionId, byte[] sessionPasswd, boolean canBeReadOnly) {
this.authInfo = new CopyOnWriteArraySet();
this.pendingQueue = new LinkedList();
this.outgoingQueue = new LinkedList();
this.sessionPasswd = new byte[16];
this.closing = false;
this.seenRwServerBefore = false;
this.eventOfDeath = new Object();
this.xid = 1;
this.state = States.NOT_CONNECTED;
this.zooKeeper = zooKeeper;
this.watcher = watcher;
this.sessionId = sessionId;
this.sessionPasswd = sessionPasswd;
this.sessionTimeout = sessionTimeout;
this.hostProvider = hostProvider;
this.chrootPath = chrootPath;
this.connectTimeout = sessionTimeout / hostProvider.size();
this.readTimeout = sessionTimeout * 2 / 3;
this.readOnly = canBeReadOnly;
this.sendThread = new ClientCnxn.SendThread(clientCnxnSocket);
this.eventThread = new ClientCnxn.EventThread();
}
发现在这个构造函数当中,重点在于ClientCnxn初始化了两个线程,用于客户端发送消息和监听事件回调
- this.sendThread = new ClientCnxn.SendThread(clientCnxnSocket)
- this.eventThread = new ClientCnxn.EventThread()
(5)还有一个需要关注的对象是ClientCnxnSocket,该类的实现类是ClientCnxnSocketNIO,该类主要通过NIO方式通道方式跟服务端进行交互,主要看doIO方法
void doIO(List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue, ClientCnxn cnxn) throws InterruptedException, IOException {
SocketChannel sock = (SocketChannel)this.sockKey.channel();
if (sock == null) {
throw new IOException("Socket is null!");
} else {
if (this.sockKey.isReadable()) {
//省略其他代码
// 从通道里读取从服务器响应回来的数据
this.sendThread.readResponse(this.incomingBuffer);
//省略其他代码
}
if (this.sockKey.isWritable()) {
synchronized(outgoingQueue) {
// 从发送消息队里里查询可以发送的packet数据包
Packet p = this.findSendablePacket(outgoingQueue, cnxn.sendThread.clientTunneledAuthenticationInProgress());
// 省略其他代码
// 发送成功后删除队列里的数据
outgoingQueue.removeFirstOccurrence(p);
// 省略其他代码
}
}
通过该类我们知道,客户端通过通道方式跟服务器进行数据交互,通过sockKey判断是否可写,如果可以,从发送队列里获取可发送的packet包,然后写入到通道里。如果是可读的方式,则从通道里读取数据,读取数据完成后,通过 this.sendThread.readResponse(this.incomingBuffer) 方法响应给客户端。通过之前getData()源码解析,在请求服务数据时候,也是通过该sendThread线程去发送数据请求。
(6)继续往下看下readResponse()方法。
void readResponse(ByteBuffer incomingBuffer) throws IOException {
//将响应回来的ByteBuffer对象转为ByteBufferInputStream
ByteBufferInputStream bbis = new ByteBufferInputStream(incomingBuffer);
// 主要做序列号和反序列化处理
BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
ReplyHeader replyHdr = new ReplyHeader();
// 反序列化数据
replyHdr.deserialize(bbia, "header");
// 省略代码。。。
// pendingQueue 等待服务器响应的队列
ClientCnxn.Packet packet;
synchronized(ClientCnxn.this.pendingQueue) {
if (ClientCnxn.this.pendingQueue.size() == 0) {
throw new IOException("Nothing in the queue, but got " + replyHdr.getXid());
}
// 删除等待队列中的packet数据包
packet = (ClientCnxn.Packet)ClientCnxn.this.pendingQueue.remove();
}
try {
//
if (packet.requestHeader.getXid() != replyHdr.getXid()) {
packet.replyHeader.setErr(Code.CONNECTIONLOSS.intValue());
throw new IOException("Xid out of order. Got Xid " + replyHdr.getXid() + " with err " + replyHdr.getErr() + " expected Xid " + packet.requestHeader.getXid() + " for a packet with details: " + packet);
}
packet.replyHeader.setXid(replyHdr.getXid());
packet.replyHeader.setErr(replyHdr.getErr());
packet.replyHeader.setZxid(replyHdr.getZxid());
// lastZxid事物id
if (replyHdr.getZxid() > 0L) {
ClientCnxn.this.lastZxid = replyHdr.getZxid();
}
// 将响应回来的数据反序列化数据
if (packet.response != null && replyHdr.getErr() == 0) {
packet.response.deserialize(bbia, "response");
}
if (ClientCnxn.LOG.isDebugEnabled()) {
ClientCnxn.LOG.debug("Reading reply sessionid:0x" + Long.toHexString(ClientCnxn.this.sessionId) + ", packet:: " + packet);
}
} finally {
// 将数据返回给客户端
ClientCnxn.this.finishPacket(packet);
}
}
重点看下 ClientCnxn.this.finishPacket(packet)
private void finishPacket(ClientCnxn.Packet p) {
// 判断下客户端packet包是否有注册监听,如果有注册监听,将注册监听事件
if (p.watchRegistration != null) {
p.watchRegistration.register(p.replyHeader.getErr());
}
if (p.cb == null) {
synchronized(p) {
p.finished = true;
p.notifyAll();
}
} else {
p.finished = true;
this.eventThread.queuePacket(p);
}
}
还记得上面讲过 watchRegistration类是其他读取数据操作类型的监听注册对象的父类,该父类提供了注册方法。
p.watchRegistration.register(p.replyHeader.getErr()) 看到这里就知道了,原来客户端注册事件是在服务器响应回来后,判断客户端是否需要注册,在将注册事件绑定到不同的监听注册对象里。
客户端回调监听事件
在ClientCnxn.this.finishPacket(packet)里,我们还看到 this.eventThread.queuePacket§,这个方法作了什么操作?
public void queuePacket(ClientCnxn.Packet packet) {
if (this.wasKilled) {
// 获取等待事件队列
LinkedBlockingQueue var2 = this.waitingEvents;
synchronized(this.waitingEvents) {
// 判断eventThread是否在运行中,如果是,将packet添加到等 // 待事件队列中
if (this.isRunning) {
this.waitingEvents.add(packet);
} else {
// 直接处理事件
this.processEvent(packet);
}
}
} else {
this.waitingEvents.add(packet);
}
}
该queuePacket方法中,判断eventThread是否处于运行状态,如果是,将服务端响应回来的数据包添加到等待事件队列中,否则直接处理响应回来的数据包。
在具体看下eventThread 事件线程类中的run方法
public void run() {
try {
this.isRunning = true;
while(true) {
// 获取等待事件队列中的对象
Object event = this.waitingEvents.take();
if (event == ClientCnxn.this.eventOfDeath) {
this.wasKilled = true;
} else {
// 处理事件,主要逻辑是这个方法
this.processEvent(event);
}
if (this.wasKilled) {
LinkedBlockingQueue var2 = this.waitingEvents;
synchronized(this.waitingEvents) {
if (this.waitingEvents.isEmpty()) {
this.isRunning = false;
break;
}
}
}
}
} catch (InterruptedException var5) {
ClientCnxn.LOG.error("Event thread exiting due to interruption", var5);
}
ClientCnxn.LOG.info("EventThread shut down for session: 0x{}", Long.toHexString(ClientCnxn.this.getSessionId()));
}
通过源码可知,客户端监听事件的主要逻辑是processEvent()
服务端如何处理监听事件?
暂定