Bootstrap

zookeeper全系列学习之统一配置获取

前言

前面介绍了zk的一些基础知识,这篇文章主要介绍下如何在java环境下获取zk的配置信息;主要基于zk的监听器以及回调函数通过响应式编程的思想将核心代码糅合成一个工具类,几乎做到了拿来即用;虽然现在一般都用nacos来做配置中心了,不会有人用zk做配置中心了,但是基于这个代码了解zk的特性还是很有必要的


分析

在分布式集群中,配置信息一般都会拎出来单独配置,这样避免了修改配置信息时候的复杂度,我们期望当有配置变更时集群中的节点能及时得到通知并作出响应;我们可以在每个节点中开启任务定时来zk获取信息,然后根据获取信息的前后是否有差异来判断信息是否变更,但是这种方法明显不及时,且每次轮询zk都会增加不必要的网络开销,特别是节点众多时给ZK增加了不必要的压力;所以我们更倾向于利用ZK的监听和回调机制来实现类似的统一消息配置,客户端只要注册一个监听器当节点信息发生变更时zk会主动触发对应事件,客户端监听对应事件作出响应即可;下面先贴出代码流程图,然后再贴出关键代码

代码执行流程图

关键代码

  • 测试类
package com.darling.service.zookeeper;

import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.ZooKeeper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * @description: 使用响应式编程思想实现基于ZK的分布式统一配置功能
 * @author: dll
 * @date: Created in 2022/11/1 12:21
 * @version:
 * @modified By:
 */
@Slf4j
public class ZkConfigTest {

    ZooKeeper zkClient;

    @Before
    public void conn (){
        zkClient  = ZkUtil.getZkClient();
    }

    @After
    public void close (){
        try {
            zkClient.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test() throws InterruptedException {
        ConfigData configData = new ConfigData();
        ZkConfigUtil watchAndCallBack = new ZkConfigUtil();
        watchAndCallBack.setZkClient(zkClient);
        watchAndCallBack.setConfigData(configData);

        watchAndCallBack.await();
        String config = configData.getConfig();

        while(true){

            if(configData.getConfig().equals("")){
                System.out.println("配置信息丢失 ......");
                watchAndCallBack.await();
            }else{
                System.out.println(configData.getConfig());

            }
            // 此处睡眠的原因是为了便于日志打印
            try {
                Thread.sleep(600);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
  • 关键工具类

package com.darling.service.zookeeper;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.zookeeper.AsyncCallback;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.util.concurrent.CountDownLatch;

/**
 * @description:
 * @author: dll
 * @date: Created in 2022/11/1 12:40
 * @version:
 * @modified By:
 */
@Data
@Slf4j
public class ZkConfigUtil implements Watcher, AsyncCallback.StatCallback, AsyncCallback.DataCallback {

    ZooKeeper zkClient;

    ConfigData configData;

    CountDownLatch cc = new CountDownLatch(1);
    
    private final String nodePath = "/config";

    /**
     * 异步获取数据getData方法的回调
     * @param rc        状态码
     * @param path      路径
     * @param ctx       上线文
     * @param data      节点的数据
     * @param stat      元数据信息
     */
    @Override
    public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
        if (data != null ){
            configData.setConfig(new String(data));
            cc.countDown();
        }
    }


    /**
     * exists方法的回调,当数据存在时会调回调函数
     * @param rc        状态码
     * @param path      路径
     * @param ctx       上下文
     * @param stat      元数据信息,可通过其是否为空判断是否有数据
     */
    @Override
    public void processResult(int rc, String path, Object ctx, Stat stat) {
        log.info("进入exists方法的回调,stat:{}");
        if (stat != null) {
            log.info("进入exists方法的回调,stat 不为空");
            // 表示path节点有数据了,可以通过getData获取
            zkClient.getData(nodePath, this, this,"123");
        }
    }


    /**
     * 节点变更的监听器
     * @param event
     */
    @Override
    public void process(WatchedEvent event) {
        switch (event.getType()) {
            case None:
                break;
            case NodeCreated:
                log.info("节点被创建,path:{}",event.getPath());
                // 节点创建后获取一遍数据还有个额外的不可忽略的作用:即重新注册了watch,否则根据zk的watch只生效一次的规则,不会再次出发watch
                zkClient.getData(nodePath, this, this,"123");
                break;
            case NodeDeleted:
                log.info("节点被删除,path:{}",event.getPath());
                configData.setConfig("");
                cc = new CountDownLatch(1);
                break;
            case NodeDataChanged:
                log.info("节点被修改,path:{}",event.getPath());
                // 节点修改获取数据的作用同节点创建
                zkClient.getData(nodePath, this, this,"123");
                break;
            case NodeChildrenChanged:
                break;
        }
    }

    public void await() {
        /**
         * 直接获取配置前先判断配置存不存在
         * 第一个参数path:配置存放的节点路径
         * 第二个参数watch:path的监听,当path有变动时回调通知
         * 第三个参数StatCallback:如果path下有数据会回调此方法,可以通过Stat是否为空判断path是否有数据
         */
        zkClient.exists(nodePath, this,this ,"PPP");
        try {
            cc.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
;