如何使用nacos在分布式环境下同步全局配置一.前言二.

一.前言

Hello,everyone.好久不见,最近需求比较多,忙到吐血,与上一篇博客的间隔时间又比较长时间了。本位将提供一种思路用来解析修改远端配置的一种操作。C端的研发大家都知道,生产环境的服务除非正常的需求迭代或者重大bug,一般情况下是不允许重启的。那么如果我想对应用内的一些配置做一些修改,这个时候该怎么办呢?

常规使用的比较多的方式是使用全局配置表,修改表即为修改配置数据,但是这种方式存在比较鸡肋的地方,每一次我修改配置都需要去修改表数据,如果分库分表的情况下,还容易造成短时间内配置不统一的情况。

我这里推荐使用nacos,apollo这种类型的配置中心,通过openApi修改远端配置中心的配置。让配置中心主动推送修改后的配置给分布式环境下的各个应用,简单高效。

二.业务场景

2.1.美团线程池

Java线程池实现原理及其在美团业务中的实践

美团技术团队提供了一种动态配合业务服务中线程池大小的策略,例如打车,早高峰晚高峰的时候订单量比较大,订单流量分析入口解析流量,达到阈值调用触发器,就可以适当的扩大线程池容量,平峰时期就可以缩小线程池容量。常规操作都是重启服务,太不友好。触发器调用修改配置中心的openApi,由配置中心主动推送修改后的参数给业务,业务重新加载远端推送过来的配置。

2.2.业务端全局配置

单库单体服务下,修改当前应用的全局配置比较简单,直接通过接口修改,数据库修改,缓存修改都是一种可行的方案。但是如果是微服务,分库分表的,分布式集群不是的情况下,主动通过接口去修改配置也不是不能实现,但是开发量比较大,还需要对各个服务的上下状态等监控。

这里如果使用接口去修改远端配置中心的配置,由配置中心去逐个修改每个服务内存中或者库表中的数据,代码量小,数据准确性高。

三.解决思路

因为博主公司使用的是nacos为配置中心。这里以nacos为例给大家展示一下如果加载与修改远端nacos的配置。

3.1.实体类定义

3.1.1.nacos配置

/**
 * @author baiyan
 * @time 2021/8/3 10:10
 */
@Data
public class NacosConfigDTO {
​
    /**
     * 命名空间
     */
    private String namespace;
​
    /**
     * 配置文件名称
     */
    private String dataId;
​
    /**
     * 配置所在组
     */
    private String group;
​
}
复制代码

3.1.2.nacos修改数据

/**
 * @author baiyan
 * @time 2021/8/3 10:10
 */
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
public class NacosValueConfigDTO extends NacosConfigDTO{
​
    /**
     * 需要修改key
     */
    private String key;
​
    /**
     * 修改后的值
     */
    private Object value;
​
}
复制代码

3.2.服务类

3.2.1.接口

/**
 * nacos配置服务
 *
 * @author baiyan
 * @time 2021/8/3 10:07
 */
public interface NacosService {
​
    /**
     * 修改nacos的配置
     * @param config
     */
    void modifyNacosConfig(NacosValueConfigDTO config);
​
    /**
     * 获取nacos的配置
     * @param config
     */
    String getNacosConfig(NacosValueConfigDTO config);
}
复制代码

3.2.2.实现类

springboot启动时初始化nacosSevice用来读取或者修改远端的配置。

/**
 * @author baiyan
 * @date 2021/08/03
 */
@Service
@Slf4j
public class NacosServiceImpl implements NacosService {
​
    @Value("${spring.cloud.nacos.config.server-addr:127.0.0.1}")
    private String serverAddr;
​
    /**
     * nacos-client
     */
    private ConfigService configService;
​
    @PostConstruct
    private void createConfigServer(){
        try {
            Properties properties = new Properties();
            properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
            configService = NacosFactory.createConfigService(properties);
        } catch (NacosException e) {
            log.error("构建nacos配置错误",e);
        }
        ValidationUtil.notNull(configService,"nacos.config.client.is.null");
    }
​
    @Override
    @SneakyThrows
    public void modifyNacosConfig(NacosValueConfigDTO config){
        String yamlValues = configService.getConfig(config.getDataId(), config.getGroup(), NacosConstant.CONNECT_EXPIRE);
        Map<String, Object> newYamlMap = YamlUtil.setValue(YamlUtil.getYamlToMap(yamlValues),config.getKey(),config.getValue());
        configService.publishConfig(config.getDataId(), config.getGroup(), YamlUtil.getYamlString(newYamlMap), ConfigType.YAML.getType());
    }
​
    @Override
    @SneakyThrows
    public String getNacosConfig(NacosValueConfigDTO config){
        String yamlValues = configService.getConfig(config.getDataId(), config.getGroup(), NacosConstant.CONNECT_EXPIRE);
        Object value = YamlUtil.getValue(config.getKey(), YamlUtil.getYamlToMap(yamlValues));
        return GsonUtil.gsonToString(value);
    }
​
}
复制代码

3.3.常量类

常量类定义需要读取或者修改的nacos中的group,dataid等配置

/**
 * nacos常量类
 *
 * @author baiyan
 */
public class NacosConstant {
​
    /**
     * 连接超时时间
     */
    public static final Integer CONNECT_EXPIRE = 5000;
​
    /**
     * 默认群组
     */
    public static final String DEFAULT_GROUP = "DEFAULT_GROUP";
​
    /**
     * 你当前这个应用的服务id
     */
    public static final String WORKFLOW_DATA_ID = "business.yaml";
​
}
复制代码

3.4.yaml解析工具类

博主的服务远端的配置中心维护的配置为yaml格式的,所以对于从远端读取到的格式需要使用yaml解析工具类解析为map格式。修改yaml配置时,逐层解析yaml配置并修改,再进行序列化成yaml格式的string,通过nacos的openApi重置远端的配置。

**
 * @author baiyan
 * @time 2021/8/3 11:37
 */
public class YamlUtil {
​
    private final static DumperOptions OPTIONS = new DumperOptions();
    private final static Yaml yaml = new Yaml();
​
    static {
        //设置yaml读取方式为块读取
        OPTIONS.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
        OPTIONS.setDefaultScalarStyle(DumperOptions.ScalarStyle.PLAIN);
        OPTIONS.setPrettyFlow(false);
    }
​
    /**
     * 将yamlMap转化成yamlString
     *
     * @param yamlObject
     * @return
     */
    public static String getYamlString(Object yamlObject) {
        return yaml.dumpAsMap(yamlObject);
    }
​
    /**
     * 将Yaml配置文件转换成map
     * @param yamlString
     * @return
     */
    public static Map<String, Object> getYamlToMap(String yamlString) {
        LinkedHashMap<String, Object> yamls = new LinkedHashMap<>();
        try {
            yamls = yaml.loadAs(yamlString, LinkedHashMap.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return yamls;
    }
​
    /**
     * 根据key获取yaml文件中属性值
     * @param key
     * @param yamlMap
     * @return
     */
    public static Object getValue(String key, Map<String, Object> yamlMap) {
        String[] keys = key.split("[.]");
        Object obj = yamlMap.get(keys[0]);
        if (key.contains(".")) {
            if (obj instanceof Map) {
                return getValue(key.substring(key.indexOf(".") + 1), (Map<String, Object>) obj);
            } else if (obj instanceof List) {
                return getValue(key.substring(key.indexOf(".") + 1), (Map<String, Object>) ((List)obj).get(0));
            } else {
                return null;
            }
        }
        return obj;
    }
​
    /**
     * 使用递归的方式更改map中的值
     * @param map
     * @param key 指定哪个键
     * @param value 即将更改的值
     * @return
     */
    public static Map<String, Object> setValue(Map<String, Object> map, String key, Object value) {
        String[] keys = key.split("\.");
        int len = keys.length;
        Map temp = map;
        for (int i = 0; i < len - 1; i++) {
            if (temp.containsKey(keys[i])) {
                Object obj = temp.get(keys[i]);
                if (obj instanceof Map) {
                    temp = (Map)obj;
                } else if (obj instanceof List) {
                    temp = (Map)((List)obj).get(0);
                } else {
                    throw new RuntimeException("temp类型错误");
                }
            } else {
                return null;
            }
            if (i == len - 2) {
                temp.put(keys[i + 1], value);
            }
        }
        for (int j = 0; j < len - 1; j++) {
            if (j == len - 1) {
                map.put(keys[j], temp);
            }
        }
        return map;
    }
​
}
复制代码

3.5.业务使用

在配置类上加上@RefreshScope

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
复制代码

例如

/**
 * nginx配置
 *
 * @author baiyan
 */
@Data
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "baiyan.nginx")
public class NginxConfig {
​
    /**
     * ip
     */
    private String endpoint;
​
    /**
     * 端口
     */
    private int port;
​
    /**
     * 协议
     * http或者https
     */
    private String protocol;
​
    /**
     * 请求前置url
     */
    private String prefixNginxUrl;
​
}
复制代码

当远端nacos配置中心对于port参数修改,那么nacos将会通过openApi推送配置给业务应用,修改port参数为远端最新参数。当业务再次去获取nginxConfig这个bean中port参数时,即为最新参数。

四.总结

文本提供了一种通过代码来修改远端配置,并同步至分布式服务的思路。解决了修改配置需要重启服务,或者配置同步开发困难的痛点。

文中如有描述不对之处,欢迎指出,共同进步~

五.联系我

文中如有不正确之处,欢迎指正,写文不易,点个赞吧,么么哒~

钉钉:louyanfeng25

微信:baiyan_lou