场景

我们经常会遇到这样的场景:

1、一个活动系统刚上线,产品经理对你说这个活动还需要额外包含一个场景,只需要添加一个字段就可以,你说等下我重新修改下再发个包。猝

2、系统连接超时设置的是300ms,大促场景下并发提高了,时延到了500ms,想要重新设置,请等下我打包。又猝

3、系统出现故障,需要启动降级服务,降级服务通常是通过一个配置来开启的,默认是关闭的,现在要开启,你说还要重新打包修改配置发布。又又猝

其实以上场景都是配置更新或者添加的场景,如果每次配置都需要系统重新发包,走测试流程,再上线,不仅效率低下,开发也无法承受。所以一种实时动态的配置中心就显得尤为重要。

系统架构

我们这里就来介绍一种实时的配置中心实现方案,先看如下架构图:

管理后台承载所有业务的配置写入,配置首先写入到DB(例如MongoDB),通过DB的主从复制,或者MongoShake写入到从数据库;

task主要负责定时从DB拉取数据写入到zookeeper;

不同的业务通过嵌入配置中心的SDK实时感知zk的配置字段变化,加载到业务内容,实现动态感知。

实现

对于后台写入到DB以及task同步到zk的实现都较为容易,这里主要讲下zk的数据存储和实时感知的实现。

先定义一个配置基于zk操作的接口configYard,里面定义配置的增删改查的各种操作,同时还包括配置重载,如下:

public interface ConfigYard {
  /**
   * 配置平台根节点名称
   */
  static String yardRoot = "/yard";
  /**
   * 初始化配置
   */
  void init();
  /**
   * 重新加载配置资源
   */
  void reload();
  /**
   * 添加配置
   * @param key
   * @param value
   */
  void add(String key, String value);
  /**
   * 更新配置
   * @param key
   * @param value
   */
  void update(String key, String value);
  /**
   * 删除配置
   * @param key
   */
  void delete(String key);
  /**
   * 获取配置
   * @param key
   * @return
   */
  String get(String key);
  /**
   * 获取所有的配置内容
   * @return
   */
  Map<String, String> getAll();
}

实现此接口,我们只讨论get和reload操作,其它的和get差不多。

public String get(String key) {
      //如果当前内存没有则访问zk获取
    if(this.yardProperties.get(key) == null){
      String contactKey = this.contactKey(key);
      if(!this.client.exists(contactKey)){
        return null;
      }
      //异步触发重载
      asyncReload();
      return this.client.readData(contactKey);
    }
    //本地内存获取
    return yardProperties.get(key);
  }

Reload的操作一般是当本地主动获取是没有以及zk监控节点发生变化是会触发,它主要是将zk监控路径下的所有key和value全部加载一遍到内存,如下实现:

public void reload() {
    List<String> yardList = this.client.getChildren(yardRoot);
    Map<String, String> currentYardProperties = new HashMap<String, String>();
    for(String yard : yardList){
      String value = this.client.readData(this.contactKey(yard));
      currentYardProperties.put(yard, value);
    }
    yardProperties = currentYardProperties;
  }

最后还需要再处理一下的就是对zk监控节点事件的处理,所有监控路径下的变动都触发reload操作。

private class ConfigYardListener implements IZkDataListener,IZkChildListener{
    public void handleDataChange(String dataPath, Object data)
        throws Exception {
      logger.info("data {} change,start reload configProperties",dataPath);
      configYard.reload();
    }

    public void handleDataDeleted(String dataPath) throws Exception {
      logger.info("data {} delete,start reload configProperties",dataPath);
      configYard.reload();
    }

    public void handleChildChange(String parentPath,
        List<String> currentChilds) throws Exception {
      logger.info("data {} ChildChange,start reload configProperties",parentPath);
      configYard.reload();
    }
  }

推荐阅读

架构实践系列: 分布式唯一ID生成方案 如何解决并发场景下扣款的数据一致性问题? 如何保障mysql和redis之间的数据一致性? 高并发系统下的缓存解决方案 Redis如何实现异地多活? 如何提升支付系统热点账户冲扣性能? 高并发高性能的DB数据复制方案 再谈Redis双活实现方案 高并发系统的限流方案如何实现? 高并发系统下的降级如何实现? 彻底弄懂Redis的内存淘汰策略

扫码关注我们

互联网架构师之路

过滤技术杂质,只为精品呈现

如果喜欢,请关注加星喔