快速入门Zookeeper实现配置中心


部分内容来自:https://bugstack.cn/md/road-map/zookeeper.html

一、环境配置

安装zookeeper

# 命令执行 docker-compose up -d
version: '3.1'
services:
  zoo1:
    image: zookeeper:3.9.0
    container_name: zookeeper
    restart: always
    hostname: zoo1#设置容器内的主机名为 zoo1
    ports:
      - 2181:2181
    environment:
      ZOO_MY_ID: 1#ZooKeeper 节点的唯一 ID,为分布式模式准备
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181#定义 ZooKeeper 集群的服务器列表,2888 是节点间通信端口,3888 是选举端口,2181 是客户端连接端口。

二、基本使用

1. 连接 zookeeper

zkCli.sh -server localhost:2181

2. 创建节点:create /path data

[zk: localhost:2181(CONNECTED) 0] create /mynode hi
Created /mynode
类似于建文件夹,用自带的命令只能一个文件夹一个文件夹地建,比如先建/mynode,再建/mynode/config

3. 创建顺序节点:create -s /path data

[zk: localhost:2181(CONNECTED) 1] create -s /mynode hello
Created /mynode0000000005

4. 创建临时顺序节点:create -e -s /path data

[zk: localhost:2181(CONNECTED) 5] create -e -s /esnode esnode-data
Created /esnode0000000007

5. 获取节点数据:get /path

[zk: localhost:2181(CONNECTED) 6] get /mynode
hi

6. 获取节点子节点列表:ls /path

[zk: localhost:2181(CONNECTED) 0] ls /mynode
[]

7. 更新节点数据:set /path data

[zk: localhost:2181(CONNECTED) 1] set /mynode mydata
[zk: localhost:2181(CONNECTED) 2] get /mynode
mydata

8. 删除节点:delete /path

删除时路径下不能有其他节点,类似于删除文件夹时里面不能有文件

9. 删除节点及其子节点:deleteall /path

直接删除完

10. 监听节点变化:get -w /path

[zk: localhost:2181(CONNECTED) 12] create /mynode
Created /mynode
[zk: localhost:2181(CONNECTED) 13] get -w /mynode
null
[zk: localhost:2181(CONNECTED) 14] set /mynode data

WATCHER::

WatchedEvent state:SyncConnected type:NodeDataChanged path:/mynode zxid: 64
[zk: localhost:2181(CONNECTED) 15] get -w /mynode
data

11. 查看节点状态:stat /path

[zk: localhost:2181(CONNECTED) 17] stat /mynode
cZxid = 0x3f
ctime = Thu Nov 28 16:51:07 UTC 2024
mZxid = 0x40
mtime = Thu Nov 28 16:51:58 UTC 2024
pZxid = 0x3f
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0

12. 查看节点ACL权限:getAcl /path

[zk: localhost:2181(CONNECTED) 18] getAcl /mynode
'world,'anyone
: cdrwa

三、启动zookeeper服务

代码来自:https://bugstack.cn/md/road-map/zookeeper.html

1. 注入zookeeper客户端

引入依赖,定义一些连接用的参数,完成连接,并往spring ioc 容器中注入zookeeper客户端bean对象

  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
      <version>3.1.4</version>
  </dependency>
# zookeeper 自定义链接配置
zookeeper:
  sdk:
    config:
      connect-string: 127.0.0.1:2181
      base-sleep-time-ms: 1000
      max-retries: 3
      session-timeout-ms: 18000
      connection-timeout-ms: 30000
@Data
@ConfigurationProperties(prefix = "zookeeper.sdk.config", ignoreInvalidFields = true)
public class ZookeeperClientConfigProperties {

    private String connectString;
    private int baseSleepTimeMs;
    private int maxRetries;
    private int sessionTimeoutMs;
    private int connectionTimeoutMs;

}
@Configuration
@EnableConfigurationProperties(ZookeeperClientConfigProperties.class)
public class ZooKeeperClientConfig {

    @Bean(name = "zookeeperClient")
    public CuratorFramework createWithOptions(ZookeeperClientConfigProperties properties) {
        ExponentialBackoffRetry backoffRetry = new ExponentialBackoffRetry(properties.getBaseSleepTimeMs(), properties.getMaxRetries());
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(properties.getConnectString())
                .retryPolicy(backoffRetry)
                .sessionTimeoutMs(properties.getSessionTimeoutMs())
                .connectionTimeoutMs(properties.getConnectionTimeoutMs())
                .build();
        client.start();
        return client;
    }

}

2. 自定义注解

DCC动态配置中心,后面用来标注哪些属性是归动态配置中心管理的

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@Documented
public @interface DCCValue {

    String value() default "";

}

3. 实现配置中心

方法作用
client.checkExists().forPath(BASE_CONFIG_PATH)检查是否存在指定节点
client.create().creatingParentsIfNeeded().forPath(BASE_CONFIG_PATH)创建节点,如果存在父节点,一起创建
CuratorCache.build(client, BASE_CONFIG_PATH)为节点创建缓存对象
curatorCache.start()启动了 CuratorCache 的监听和缓存功能
curatorCache.listenable().addListener()添加监听
@Slf4j
@Configuration
public class DCCValueBeanFactory implements BeanPostProcessor {

    private static final String BASE_CONFIG_PATH = "/paran/config";

    private final CuratorFramework client;

    private final Map<String, Object> dccObjGroup = new HashMap<>();

    public DCCValueBeanFactory(CuratorFramework client) throws Exception {
        this.client = client;

        // 节点判断
        if (null == client.checkExists().forPath(BASE_CONFIG_PATH)) {
            client.create().creatingParentsIfNeeded().forPath(BASE_CONFIG_PATH);
            log.info("DCC 节点监听 base node {} not absent create new done!", BASE_CONFIG_PATH);
        }

        CuratorCache curatorCache = CuratorCache.build(client, BASE_CONFIG_PATH);
        curatorCache.start();

        curatorCache.listenable().addListener((type, oldData, data) -> {
            switch (type) {
                case NODE_CHANGED:
                    String dccValuePath = data.getPath();
                    Object objBean = dccObjGroup.get(dccValuePath);
                    try {
                        // 1. getDeclaredField 方法用于获取指定类中声明的所有字段,包括私有字段、受保护字段和公共字段。
                        // 2. getField 方法用于获取指定类中的公共字段,即只能获取到公共访问修饰符(public)的字段。
                        Field field = objBean.getClass().getDeclaredField(dccValuePath.substring(dccValuePath.lastIndexOf("/") + 1));
                        field.setAccessible(true);
                        field.set(objBean, new String(data.getData()));
                        field.setAccessible(false);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                    break;
                default:
                    break;
            }
        });
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class<?> beanClass = bean.getClass();
        Field[] fields = beanClass.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(DCCValue.class)) {
                DCCValue dccValue = field.getAnnotation(DCCValue.class);

                try {
                    if (null == client.checkExists().forPath(BASE_CONFIG_PATH.concat("/").concat(dccValue.value()))) {
                        client.create().creatingParentsIfNeeded().forPath(BASE_CONFIG_PATH.concat("/").concat(dccValue.value()));
                        log.info("DCC 节点监听 listener node {} not absent create new done!", BASE_CONFIG_PATH.concat("/").concat(dccValue.value()));
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }

                dccObjGroup.put(BASE_CONFIG_PATH.concat("/").concat(dccValue.value()), bean);
            }
        }
        return bean;
    }


}

1. postProcessAfterInitialization

方法作用
isAnnotationPresent(xxx.class)判断某个字段上是否有xxx注解
getAnnotation(xxx.class)返回xxx注解的实例对象

关于反射的知识请看https://blog.csdn.net/fim77/article/details/143796031

实现这个接口,重载postProcessAfterInitialization方法,即可对spring bean对象初始化完成之后做一些后续处理

public class DCCValueBeanFactory implements BeanPostProcessor

通过反射获取bean的属性字段

Class<?> beanClass = bean.getClass();
Field[] fields = beanClass.getDeclaredFields();

通过isAnnotationPresent()方法判断属性字段上有没有指定注解

field.isAnnotationPresent(DCCValue.class)

如果有的话,把注解对象拿出来,这样后续就可以用到之前注解中写的值了

DCCValue dccValue = field.getAnnotation(DCCValue.class)

postProcessAfterInitialization部分的流程如下
请添加图片描述

2. DCCValueBeanFactory构造函数

请添加图片描述

四、功能使用

@RestController
public class ConfigController {

    @DCCValue("downgradeSwitch")
    private String downgradeSwitch;

    @DCCValue("userWhiteList")
    private String userWhiteList;

    @Resource
    private CuratorFramework curatorFramework;

    /**
     * curl http://localhost:8091/getConfig/downgradeSwitch
     */
    @RequestMapping("/getConfig/downgradeSwitch")
    public String getConfigDowngradeSwitch() {
        return downgradeSwitch;
    }

    /**
     * curl http://localhost:8091/getConfig/userWhiteList
     */
    @RequestMapping("/getConfig/userWhiteList")
    public String getConfigUserWhiteList() {
        return userWhiteList;
    }

    /**
     * curl -X PUT "http://localhost:8091/setConfig?downgradeSwitch=true&userWhiteList=paran"
     */
    @PutMapping("/setConfig")
    public void setConfig(Boolean downgradeSwitch, String userWhiteList) throws Exception {
        curatorFramework.setData().forPath("/paran/config/downgradeSwitch", (downgradeSwitch ? "开" : "关").getBytes(StandardCharsets.UTF_8));
        curatorFramework.setData().forPath("/paran/config/userWhiteList", userWhiteList.getBytes(StandardCharsets.UTF_8));
    }

}

测试结果

C:\Users\Paran>curl http://localhost:8091/getConfig/downgradeSwitch

C:\Users\Paran>curl http://localhost:8091/getConfig/userWhiteList

C:\Users\Paran>curl -X PUT "http://localhost:8091/setConfig?downgradeSwitch=true&userWhiteList=paran"

C:\Users\Paran>curl http://localhost:8091/getConfig/downgradeSwitch
开
C:\Users\Paran>curl http://localhost:8091/getConfig/userWhiteList
paran
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值