文章目录

  • 1. 配置服务的作用
  • 2. 配置服务需要完成的工作
  • 3. 使用zookeeper实现简单的配置服务
  • 3.1. 功能点
  • 3.2. 简单代码实现
  • 3.2.1. 基本类
  • 3.2.2. 启动时注册配置项监听器
  • 3.2.3. 配置ConfigManager
  • 3.3. 不足及改进方案
  • 3.3.1. 监听器注册
  • 3.3.2. 配置文件管理


1. 配置服务的作用

一般我们把配置写在配置文件中,但在分布式系统中,各个系统分布在各个不同的服务器中
这极大的增加了修改配置文件的工作量,且手动更新无法保证配置文件的一致性和及时性
为了解决这个问题,有人提出了配置服务的概念,当配置发生变更时,所有服务收到通知并进行相对应的响应

2. 配置服务需要完成的工作

  • 在配置变更时通知节点
  • 在配置发生时进行处理
  • 能对配置进行集中管理

3. 使用zookeeper实现简单的配置服务

3.1. 功能点

  • 启动时自动加载配置,如果zk中没有加载默认并设置到zk
  • 启动时注册watch到zk中的系统配置节点
  • 为每个配置项注册监听器,在配置发生变更时进行相应的操作
  • 在数据变更时,触发数据相应的监听器

3.2. 简单代码实现

3.2.1. 基本类

  • Listener
public interface Listener<T> {
     void process(String config,T newValue,T oldValue,ChangeType changeType);
}
  • ChangeType
public enum  ChangeType {
    UPDATE,
    DELETE,
    ADD,
    NONE
}
  • ConfigManager
public class ConfigManager implements InitializingBean {

    private Map<String,Object> configMap;

    private Map<String, Listener> listenerMap;

    private String zookeeperPath;

    private ZooKeeper zk;

    private ObjectMapper objectMapper;

    public void regiest(String key, Listener listenter){
        if(listenerMap == null){
            listenerMap = new HashMap<>();
        }
        listenerMap.put(key,listenter);
    }


    private void configUpudate(Map<String,Object> configMap){
        if(this.configMap == null){
            configMap.forEach((key,value)->{
                triggerListenter(key,null,value);
            });

        }else {
            Set<String> keySet = new HashSet();
            keySet.addAll(configMap.keySet());
            keySet.addAll(this.configMap.keySet());

            keySet.forEach(item -> {
                Object oldValue = this.configMap.get(item);
                Object newValue = configMap.get(item);
                triggerListenter(item, oldValue, newValue);
            });
        }
        this.configMap = configMap;
    }

    private void triggerListenter(String key,Object oldValue,Object newValue){
        if(listenerMap == null) return;
        Listener listenter = listenerMap.get(key);
        if(listenter == null) {
            return;
        }
        ChangeType changeType = ChangeType.NONE;
        if(oldValue == null&& newValue == null){
            changeType = ChangeType.NONE;
        }else if(oldValue == null){
            changeType = ChangeType.ADD;
        }else if(newValue == null){
            changeType = ChangeType.DELETE;
        }else if(oldValue!=null&&newValue!=null&&!oldValue.equals(newValue)){
            changeType = ChangeType.UPDATE;
        }else{
            return;
        }
        listenter.process(key,newValue,oldValue,changeType);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        loadConfig();
        Watcher watcher = new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                System.out.println("数据发生变化了");
                try {
                    byte[] data = zk.getData(zookeeperPath, false, new Stat());
                    HashMap configMap = objectMapper.readValue(data, HashMap.class);
                    configUpudate(configMap);
                    zk.exists(zookeeperPath, this);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        };

        zk.exists(zookeeperPath,watcher);

    }

    public Map<String, Object> getConfigMap() {
        return configMap;
    }

    public void setConfigMap(Map<String, Object> configMap) {
        this.configMap = configMap;
    }

    public Map<String, Listener> getListenerMap() {
        return listenerMap;
    }


    public String getZookeeperPath() {
        return zookeeperPath;
    }

    public void setZookeeperPath(String zookeeperPath) {
        this.zookeeperPath = zookeeperPath;
    }

    public ZooKeeper getZk() {
        return zk;
    }

    public void setZk(ZooKeeper zk) {
        this.zk = zk;
    }

    public void setObjectMapper(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    public void loadConfig(){
        try {
            zk.create("/sumanit", "sumanit".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }catch (Exception e){

        }
        try {
            Map configMap = new HashMap();
            configMap.put("server.port",8080);
            byte[] bytes = objectMapper.writeValueAsBytes(configMap);
            zk.create("/sumanit/config", bytes, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            setConfigMap(configMap);
        }catch (Exception e){

        }
    }
}

3.2.2. 启动时注册配置项监听器

@Component
public class Runner implements CommandLineRunner {
    @Autowired
    private ConfigManager configManager;
    @Override
    public void run(String... args) throws Exception {
        configManager.regiest("server.port", new Listener() {
            @Override
            public void process(String config, Object newValue, Object oldValue, ChangeType changeType) {
                System.out.println("newValue:"+newValue);
            }
        });
    }
}

3.2.3. 配置ConfigManager

@Configuration
public class ConfigManagerConfiguration {
    @Autowired
    private ObjectMapper objectMapper;

    @Bean
    public ConfigManager configManager(){
        ConfigManager configManager = new ConfigManager();
        try {
            ZooKeeper zk = new ZooKeeper("127.0.0.1:2181", 5000, new Watcher() {
                @Override
                public void process(WatchedEvent watchedEvent) {
                    System.out.println("连接成功 ");
                }
            }, true);


            configManager.setZk(zk);
            configManager.setZookeeperPath("/sumanit/config");
            configManager.setObjectMapper(objectMapper);
        }catch (Exception e){
            e.printStackTrace();
        }
        return configManager;
    }
}

3.3. 不足及改进方案

3.3.1. 监听器注册

需要单独为每个配置手动注册,每增加一个配置项就要增加一个监听器

其实大部分的配置都是直接一个属性值的设置,可以考虑给这个配置增加一个注解

然后当值发生变化时,通过Spring获取到该对象然后利用反射修改配置的值

也有一些特殊的操作需要去做,比如连接ip发生了变化,现在连接的客户端都要重连

3.3.2. 配置文件管理

可以考虑两种配置文件的管理,一种是使用本地文件,一种是使用git

  • 本地文件

当修改了文件之后,调用一个接口,然后提交给zk

加载时也是,如果zk没有配置,那么加载本地并设置,如果有使用zk的配置

当zk修改后,也要将配置同步到本地文件中

  • 使用git

原理是使用git的hook实现,当文件修改时会调用对应的url

接收到请求后去git获取文件并设置到zk中就可以了

加载时也默认去git中加载