【zookeeper watcher源码解析】

前言

  1. zookeeper 为啥要引入watcher机制?
  2. watcher机制解决了什么样的问题?
  3. watcher机制使用的场景在哪里?
  4. 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提供了两个属性,wathcerpath,当客户端注册事件时,构造函数将注册事件和节点路径对应起来,当某个节点增加监听对象时,在把该监听对象放在节点对应的监听集合里。
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()

服务端如何处理监听事件?

暂定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值