zokkepeer watcher机制

1.简介

Zookeeper采用了Watcher机制实现数据的发布/订阅功能。该机制在被订阅对象发生变化时会异步通知客户端。

可以看作观察者模式在分布式场景下的实现,特征如下。

  • 一次性:3.6版本之前所有事件是一次性的,3.6新增持久watcher和持久递归watcher。
  • 轻量级:WatchEvent是最小的通信单元,结构上只包含通知状态、事件类型和节点路径。
  • 客户端串行触发执行:注意回调函数的处理时间,避免阻塞。
  • 时效性:会话有效。

Watcher内部包含了两个枚举类

  • KeeperState:Disconnected、SyncConnected、AuthFailed、ConnectedReadOnly、SaslAuthenticated、Expired、Closed
  • EventType:None、NodeCreated、NodeDeleted、NodeDataChanged、NodeChildrenChanged、DataWatchRemoved、ChildWatchRemoved、PersistentWatchRemoved

注册使用:可以通过 getData(),getChildren() 和 exists() 三个方法来设置 watcher

2.使用demo

官方demo:https://zookeeper.apache.org/doc/r3.6.0/javaExample.html

3.原理

简单理解就是一个分布式的观察者模式实现。涉及到跨主机通信,所以内部使用nio和netty两种方式进行rpc。

以getData注册watcher为例:

客户端通过rpc向服务端进行watcher注册,成功后在本地的ZKWatchManager保存节点路径到watcher的映射。同理,服务端也会在自己的WatchManager里面保存一份映射。

客户端调用setData后,服务端WatchManager找到对应节点的watcher,通过rcp通知客户端执行watcher的process方法实现回调逻辑。


(1)两个WatchManager

客户端 ZKWatchManager

//ZKWatchManager维护了5个map,key代表数据节点的绝对路径,value代表注册在当前节点上的watcher集合

//节点上内容数据、状态信息变更相关监听
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>>();

//3.6版本新增了持久watcher和持久递归watcher
private final Map<String, Set<Watcher>> persistentWatches = new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> persistentRecursiveWatches = new HashMap<String, Set<Watcher>>();

服务端 WatchManager

//节点路径对应的所有watcher映射
private final Map<String, Set<Watcher>> watchTable = new HashMap<>();
//watcher对应的所有节点路径的映射
private final Map<Watcher, Set<String>> watch2Paths = new HashMap<>();

(2)rpc通信

nio和netty两种方式


(3)wacher注册和触发流程

 

 图片来自:参考(2)


4常见的问题

(1)watcher是一次性的吗?

3.6版本之前的都是一次性的,3.6新增持久watcher和持久递归watcher。注意其他封装类型的客户端的版本支持(如cursor从5.0开始支持)

一次性是因为WatchManager每次处理完后都会remove掉对应的watcher,这么设计是为了减轻系统负担。

(2)zkClient使用监听器封装了watcher

直接实现IZkChildListener、IZkChildListener、IZkStateListener等接口。而且不需要重复注册(watcher的一次性)

(3)已经注册的watcher会丢吗?什么情况下会丢

有可能。

发生CONNECTIONLOSS之后,只要在session_timeout之内再次连接上(即不发生SESSIONEXPIRED),那么这个连接注册的watches依然在,否则就会丢失。

(4)能否收到每次节点变化的通知

在分布式多客户端情况下节点数据的更新频率很高的话,不能。

因为处理watcher回调到再次注册watcher期间,服务端节点可能变化很多次了。

(5)同一个zk客户端对某一个节点注册相同的watch,只会收到一次通知。


zookeeper使用watcher实现分布式锁

package com.wanna.zk.zkstudy;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.LockSupport;

/**
 * 利用zk实现分布式锁思路:使用临时顺序节点
 * 1.在执行加锁之前,先判断/LOCKS有没有被创建,如果没有则先创建
 * 2.如果创建了/LOCKS,则创建/LOCKS/LOCK_xxxxxxxx这样子的临时顺序节点
 * 3.尝试去加锁的逻辑,先获取/LOCKS/下所有的孩子节点,并进行排序
 * ---3.1如果当前锁在的位置是0,也就是第一个元素,那么就获取锁成功,开始执行业务代码
 * ---3.2如果当前锁所在的位置不是0,那么就获取它的前一个节点,并进行监控
 * ------如果前一个节点已经被删了,那么就继续尝试去获取锁
 * ------如果前一个节点还没被删,那么就阻塞当前线程,直到监听器(Watcher)对象将其唤醒才继续执行
 * 4.解锁逻辑,删除自己创建的LOCK这条记录,因为是临时顺序节点,因此就算机器宕机了也会自动删除锁
 */
public class ZKLock {
    private static final String LOCK_ROOT = "/LOCKS";   //锁的根路径
    private static final String LOCK_NODE_NAME = "LOCK_";  //锁的名称,使用临时顺序节点
    private String lockPath;  //完整的锁路径
    //集群的连接字符串,机器ip:port中间用逗号分隔即可
    String connectString = "localhost:2181,localhost:2182,localhost:2183";  
    private ZooKeeper zooKeeper;  //zk对象
    private final Thread currentThread = Thread.currentThread();  //获取创建对象的线程,用来进行唤醒和阻塞
    private int zkSessionTimeout = 5000;  //sessionTimeout

    //判断某个元素是否被删除,如果删除了就将当前线程唤醒
    Watcher watcher = new Watcher() {
        @Override
        public void process(WatchedEvent event) {
            //如果获取到删除节点的事件,那么就唤醒当前线程
            if (event.getType() == Event.EventType.NodeDeleted) {
                LockSupport.unpark(currentThread);  //将当前线程唤醒
            }
        }
    };

    public ZKLock() {
        try {
            zooKeeper = new ZooKeeper(connectString, zkSessionTimeout, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (event.getType() == Event.EventType.None) {
                        //如果链接建立成功,打印相关信息
                        if (event.getState() == Event.KeeperState.SyncConnected) {
                            System.out.println("ZK连接建立成功");
                            LockSupport.unpark(currentThread);
                        }
                    }
                }
            });
            LockSupport.park();   //因为是异步去进行连接,因此这里需要等待
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void lock() {
        try {
            createLock();  //createLock
            attemptLock();  //尝试去获取锁
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void createLock() throws KeeperException, InterruptedException {
        //判断LOCKS节点是否存在
        final Stat exists = zooKeeper.exists(LOCK_ROOT, false);
        //如果不存在则创建一个/LOCKS的持久节点
        if (exists == null) {
            zooKeeper.create(LOCK_ROOT, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        //创建临时有序节点,节点为/LOCKS/LOCK_xxxxxxxx
        lockPath = zooKeeper.create(LOCK_ROOT.concat("/").concat(LOCK_NODE_NAME), new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        System.out.println("LOCK NODE ".concat(lockPath).concat(" has been created"));
    }

    //尝试去获取锁
    public void attemptLock() throws KeeperException, InterruptedException {
        //获取/LOCKS节点的孩子节点
        final List<String> children = zooKeeper.getChildren(LOCK_ROOT, false);
        //对children节点进行排序
        Collections.sort(children);
        //获取当前锁节点排序之后的下标,截取掉/LOCKS/之后的内容
        final int index = children.indexOf(lockPath.substring(LOCK_ROOT.length() + 1));
        //如果获取到的index为0,也就是第一个元素
        if (index == 0) {
            System.out.println("获取LOCK成功");
            return;
        }
        //如果不是第一个元素,获取上一个节点的路径
        final String s = children.get(index - 1);
        //监视它的上一个元素的变化情况,传入watcher对象
        final Stat exists = zooKeeper.exists(LOCK_ROOT.concat("/").concat(s), watcher);
        //继续去获取锁
        if (exists != null) {  //如果exists不为null,那么就阻塞,不然就尝试去获取锁
            LockSupport.park();
        }
        attemptLock();
    }

    public void unlock() {
        //删除锁的临时有序节点,并且关闭连接对象
        try {
            zooKeeper.delete(lockPath, -1);
            zooKeeper.close();
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }
}

参考

(1)https://www.jianshu.com/p/c68b6b241943

(2)https://www.nightfield.com.cn/index.php/archives/189/

(3)https://zookeeper.apache.org/doc/r3.6.0/javaExample.html

(4)https://www.jianshu.com/p/7760a9d77a1f

posted @ 2022-06-21 21:16  OUYM  阅读(37)  评论(0编辑  收藏  举报