一.前言
Hello,everyone.好久不见,最近需求比较多,忙到吐血,与上一篇博客的间隔时间又比较长时间了。本位将提供一种思路用来解析修改远端配置的一种操作。C端的研发大家都知道,生产环境的服务除非正常的需求迭代或者重大bug,一般情况下是不允许重启的。那么如果我想对应用内的一些配置做一些修改,这个时候该怎么办呢?
常规使用的比较多的方式是使用全局配置表,修改表即为修改配置数据,但是这种方式存在比较鸡肋的地方,每一次我修改配置都需要去修改表数据,如果分库分表的情况下,还容易造成短时间内配置不统一的情况。
我这里推荐使用nacos,apollo这种类型的配置中心,通过openApi修改远端配置中心的配置。让配置中心主动推送修改后的配置给分布式环境下的各个应用,简单高效。
二.业务场景
2.1.美团线程池
Java线程池实现原理及其在美团业务中的实践
美团技术团队提供了一种动态配合业务服务中线程池大小的策略,例如打车,早高峰晚高峰的时候订单量比较大,订单流量分析入口解析流量,达到阈值调用触发器,就可以适当的扩大线程池容量,平峰时期就可以缩小线程池容量。常规操作都是重启服务,太不友好。触发器调用修改配置中心的openApi,由配置中心主动推送修改后的参数给业务,业务重新加载远端推送过来的配置。
2.2.业务端全局配置
单库单体服务下,修改当前应用的全局配置比较简单,直接通过接口修改,数据库修改,缓存修改都是一种可行的方案。但是如果是微服务,分库分表的,分布式集群不是的情况下,主动通过接口去修改配置也不是不能实现,但是开发量比较大,还需要对各个服务的上下状态等监控。
这里如果使用接口去修改远端配置中心的配置,由配置中心去逐个修改每个服务内存中或者库表中的数据,代码量小,数据准确性高。
三.解决思路
因为博主公司使用的是nacos为配置中心。这里以nacos为例给大家展示一下如果加载与修改远端nacos的配置。
3.1.实体类定义
3.1.1.nacos配置
代码语言:javascript复制/**
* @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修改数据
代码语言:javascript复制/**
* @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.接口
代码语言:javascript复制/**
* 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用来读取或者修改远端的配置。
代码语言:javascript复制/**
* @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等配置
代码语言:javascript复制/**
* 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重置远端的配置。
代码语言:javascript复制**
* @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
代码语言:javascript复制@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
例如
代码语言:javascript复制/**
* 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参数时,即为最新参数。
四.总结
文本提供了一种通过代码来修改远端配置,并同步至分布式服务的思路。解决了修改配置需要重启服务,或者配置同步开发困难的痛点。
文中如有描述不对之处,欢迎指出,共同进步~