zookeeper-配置中心

平常工作中经常使用公司的配置中心修改特定服务的配置,修改配置后,部署在不同机房不同机器的服务都能够及时跟随配置的变化。这个功能就是通过zookeeper实现的,同时结合了spring、java自定义注解,便于业务根据线上情况灵活调整。

本文介绍通过zookeeper的api实现简单的服务监听配置功能。

1. 代码

代码包括如下三个部分:配置客户端类ConfigClient、应用服务类Service、本地配置类MyConfig,关系如下:

ConfigClient通过与zookeeper服务器交互,修改MyConfig;

Service在业务逻辑中直接使用MyConfig;

Service启动时,完成ConfigClient的启动、初始化。

package config;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/**
 * 实际生产中,配置中心的客户端以java注解的形式使用
 * 通过maven引入依赖,随着服务的启动,spring容器自动加载
 */
public class ConfigClient {

    private static final Logger log = LoggerFactory.getLogger(ConfigClient.class);

    private ZooKeeper zkClient;

    private String servicePath;

    private Map<String, String> localConfigs;

    /**
     * 服务启动时调用,将服务节点记录到/config-center节点下
     */
    public void init(String appkey) throws IOException {
        zkClient = new ZooKeeper("127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183", 15000,
                event -> log.info("event:{}", event));
        this.servicePath = "/config-center/" + appkey;
        createNode(servicePath, new byte[0]);
        localConfigs = new HashMap<>(10);
    }

    /**
     * 注意,调用create时,data还被当做上下文参数ctx的值;在异步回调时,如果发生connectionLoss,ctx可以作为data再次调用create方法
     */
    private void createNode(String path, byte[] data) {
        zkClient.create(path, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, new AsyncCallback.StringCallback() {
            @Override
            public void processResult(int rc, String path, Object ctx, String name) {
                switch (KeeperException.Code.get(rc)) {
                    case CONNECTIONLOSS:
                        createNode(path, (byte[]) ctx);
                        break;
                    case OK:
                        log.info("{} create successfully!", path);
                        break;
                    case NODEEXISTS:
                        log.info("{} exists!", path);
                        break;
                    default:
                        log.error("Something went wrong: ", KeeperException.create(KeeperException.Code.get(rc), path));
                }
            }
        }, data);
    }

    /**
     * 服务启动时调用,将本地的每个配置项作为节点,记录到服务节点下
     */
    public void registerConfig(String fieldName, Object configValue) {
        byte[] data = configValue == null ? new byte[0] : configValue.toString().getBytes();
        String configPath = this.servicePath + "/" + fieldName;
        createNode(configPath, data);
        localConfigs.put(configPath, fieldName);
        reloadConfig(configPath);
    }

    /**
     * DataCallback:异步方式查询数据,如果连接丢失,重新执行reloadConfig
     * Watcher:监听节点数据变更,即监听配置的值发生变更
     */
    public void reloadConfig(String configPath) {
        zkClient.getData(configPath, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                switch (event.getType()) {
                    case NodeDataChanged:
                        log.info("数据修改事件发生:{}", event.getPath());
                        reloadConfig(event.getPath());
                }
            }
        }, new AsyncCallback.DataCallback() {
            @Override
            public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
                switch (KeeperException.Code.get(rc)) {
                    case CONNECTIONLOSS:
                        reloadConfig(path);
                    case OK:
                        String fieldName = localConfigs.get(path);
                        String strValue = new String(data);
                        reloadValue(fieldName, strValue);
                }

            }
        }, new Stat());
    }

    /**
     * 通过反射的方式,将配置的变更set到应用服务的MyConfig类的对应field中
     * 实际生产中,依托配置中心的客户端field级注解:不必将所有配置统一写到特定的类中;配置项名称不必与字段名称fieldName相同
     */
    private void reloadValue(String fieldName, String strValue) {
        try {
            Field field = MyConfig.class.getDeclaredField(fieldName);
            field.setAccessible(true);
            if (field.getType() == String.class) {
                field.set(MyConfig.getInstance(), strValue);
            } else if (field.getType() == Integer.class) {
                field.set(MyConfig.getInstance(), Integer.valueOf(strValue));
            }
        } catch (IllegalAccessException e) {
            log.error("reload zk config to local config fail:", e);
        } catch (NoSuchFieldException e) {
            log.error("reload zk config to local config fail:", e);
        }
    }

}
package config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Scanner;

public class Service {

    private static final Logger log = LoggerFactory.getLogger(ConfigClient.class);

    /**
     * 服务唯一标识
     */
    private String appkey = "service-1";

    private ConfigClient configClient;

    /**
     * 应用服务初始化
     * 在实际生产中,通过spring完成configClient的初始化、依赖注入
     */
    public void init() throws IOException {
        configClient = new ConfigClient();
        configClient.init(appkey);
        configClient.registerConfig("whiteList", MyConfig.getInstance().whiteList);
        configClient.registerConfig("limit", MyConfig.getInstance().limit);
    }

    public static void main(String[] args) {
        Service service = new Service();
        try {
            service.init();
            Thread.sleep(2000);
        } catch (Exception e) {
            log.error("start fail....");
            System.exit(-1);
        }
        Scanner sc = new Scanner(System.in);
        while (true) {
            log.info("回车查看当前配置:");
            sc.nextLine();
            log.info("service config whiteList: {}, limit:{}", MyConfig.getInstance().getWhiteList(),
                    MyConfig.getInstance().limit);
        }
    }
}
package config;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 实际生产中:
 * ① myConfig可以作为单例bean托管在spring容器中
 * ② 利用java field级注解,不必将每一项配置统一写到特定类中,
 */
public class MyConfig {

    private volatile static MyConfig instance;

    private MyConfig() {
    }

    /**
     * 单例模式:延迟加载与双重检查锁定
     */
    public static MyConfig getInstance() {
        if (instance == null) {
            synchronized (MyConfig.class) {
                if (instance == null) {
                    instance = new MyConfig();
                }
            }
        }
        return instance;
    }

    /**
     * 配置1:白名单列表
     */
    public String whiteList = "lileim,hanmeimei";

    public List<String> getWhiteList() {
        try {
            return new ArrayList(Arrays.asList(whiteList.split(",")));
        } catch (Exception e) {
            return new ArrayList<>();
        }
    }

    /**
     * 配置2:限制
     */
    public Integer limit = 10;

}

2. 运行

(1)命令行启动zookeeper服务器,指令如下:

zkServer.sh start zoo-1.cfg
zkServer.sh start zoo-2.cfg
zkServer.sh start zoo-3.cfg

(2)命令行启动zookeeper客户端,新建配置中心节点/config-center

admindeMacBook-Pro-36:~ yeleits$  zkCli.sh -server 127.0.0.1:2181
[zk: 127.0.0.1:2181(CONNECTED) 0] create /config-center

(3)运行Service的main方法,控制台日志如下:

服务启动,第一次加载服务节点/config-center/service-1、配置节点/config-center/service-1/whiteList、/config-center/service-1/limit,配置节点的值是MyConfig类中对应字段的初始值。

(4)zookeeper客户端执行如下指令,并观察Service的控制台日志:

[zk: 127.0.0.1:2181(CONNECTED) 3] get /config-center/service-1/whiteList
lileim,hanmeimei

[zk: 127.0.0.1:2181(CONNECTED) 5] set /config-center/service-1/whiteList "zhangsan,lisi"

zookeeper客户端修改配置,服务成功监听到变化,修改了本地的配置值。

(5)停止Service后重启,观察Service的控制台日志:

服务重启后,提示服务节点、配置节点都已经存在了,而配置值不是代码中的默认初值。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值