nacos动态刷新原理
Nacos是什么
Nacos 致力于发现、配置和管理微服务。它提供了一组简单易用的特性集,帮助快速实现动态服务发现、服务配置、服务元数据及流量管理。使用Nacos 可以更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
服务(Service)是 Nacos 世界的一等公民。Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理。
Nacos 的关键特性包括:
服务发现和服务健康监测 ,支持基于DNS和RPC的服务发现,支持基于传输层和应用层的监控检查;
动态配置服务 ,可以以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置,同时支持版本追踪和一键回滚等功能;
动态 DNS 服务 ,动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务;
服务及其元数据管理 ,管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。
基本架构
逻辑架构
NACOS动态配置
方式一:启动配置管理方式获取配置
1、项目引入POM包
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId><version>2.2.2.RELEASE</version></dependency>
2、yml配置NACOS系统信息
新增bootstrap.yml文件,配置信息写在该文件里。(问题:如放在application.yml会导致项目启动报找不到配置属性错误,原因:application.yml与bootstrap.yml加载顺序优先级问题。)
bootstrap.yml(bootstrap.properties)用来程序引导时执行,应用于更加早期配置信息读取,如可以使用来配置application.yml中使用到参数等application.yml(application.properties) 应用程序特有配置信息,可以用来配置后续各个模块中需使用的公共参数等。加载顺序:bootstrap.yml > application.yml > application-dev(prod).yml > ..
在bootstrap.yml中新增application.name和nacos的config信息。
spring:application:name: order-service-democloud:nacos:config:: localhost:8848: properties # 此处为配置使用的后缀名group: DEFAULT_GROUP
3、NACOS系统新增动态配置参数
登录NOCAS系统在配置列表页面,点击新增配置输入配置信息,这里选择的是properties配置文件类型。
DataID格式:${prefix}-${spring.profiles.active}.${file-extension}
prefix:默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix来配置。spring.profiles.active:即为当前环境对应的 profile,详情可以参考 Spring Boot文档。 注意:当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}file-exetension:为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型。
4、代码配置
在使用配置的controller中新增 @RefreshScope 注解,以及在注入属性上新增 @Value("${Key名称}") 注解。访问网站处呈现
方式二:JAVA SDK方式获取配置
1、项目引入POM包
<dependency><groupId>com.alibaba.nacos</groupId><artifactId>nacos-client</artifactId><version>2.0.0-ALPHA.2</version></dependency>
2、NACOS系统新增动态配置参数
登录NOCAS系统在配置列表页面,点击新增配置输入配置信息,这里选择的是json配置文件类型。
DataID格式:${prefix}-${spring.profiles.active}.${file-extension}
prefix:默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix来配置。spring.profiles.active:即为当前环境对应的 profile,详情可以参考 Spring Boot文档。 注意:当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}file-exetension:为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型。
3、代码段
获取NACOS配置服务,根据Data ID获取配置。
获取到的配置信息,接口返回呈现。
为什么RefreshScope能刷新配置
看看org.springframework.cloud.context.scope.refresh.RefreshScope这个类
public class RefreshScope extends GenericScope implements ApplicationContextAware, Ordered {...public boolean refresh(String name) {if (!name.startsWith("scopedTarget.")) {name = "scopedTarget." + name;}if (super.destroy(name)) {this.context.publishEvent(new RefreshScopeRefreshedEvent(name));return true;} else {return false;}}public void refreshAll() {super.destroy();this.context.publishEvent(new RefreshScopeRefreshedEvent());}...}
它在调用refresh方法的时候,会去调用工厂摧毁已生成的bean对象
看看它的父类GenericScope:
public class GenericScope implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {...private GenericScope.BeanLifecycleWrapperCache cache = new GenericScope.BeanLifecycleWrapperCache(new StandardScopeCache());...protected boolean destroy(String name) {GenericScope.BeanLifecycleWrapper wrapper = this.cache.remove(name);if (wrapper != null) {Lock lock = ((ReadWriteLock)this.locks.get(wrapper.getName())).writeLock();lock.lock();try {wrapper.destroy();} finally {lock.unlock();}this.errors.remove(name);return true;} else {return false;}}...public Object get(String name, ObjectFactory<?> objectFactory) {GenericScope.BeanLifecycleWrapper value = this.cache.put(name, new GenericScope.BeanLifecycleWrapper(name, objectFactory));this.locks.putIfAbsent(name, new ReentrantReadWriteLock());try {return value.getBean();} catch (RuntimeException var5) {this.errors.put(name, var5);throw var5;}}public Object getBean() {if (this.bean == null) {String var1 = this.name;synchronized(this.name) {if (this.bean == null) {this.bean = this.objectFactory.getObject();}}}return this.bean;}...}
这个类中有一个成员变量BeanLifecycleWrapperCache,用于缓存所有已经生成的Bean,在调用get方法时尝试从缓存加载,如果没有的话就生成一个新对象放入缓存,并通过初始化getBean其对应的Bean.
清空缓存后,下次访问对象时就会重新创建新的对象并放入缓存了。
所以在重新创建新的对象时,也就获取了最新的配置, 也就达到了配置刷新的目的.
@NacosPropertySource自动刷新原理
在@NacosPropertySource的自动刷新中,ClientWorker类起着非常关键的作用,其作用如下:
当设置autoRefreshed = true,ClientWorker提供了cacheMap在本地缓存这些需要自动刷新的配置数据;
提供了从服务端和本地获取配置数据的方法,并提供一个定时任务,定时从服务端拉取配置数据。
先来看一下ClientWorker的构造方法:
public ClientWorker(final HttpAgent agent, final ConfigFilterChainManager configFilterChainManager,final Properties properties) {//代码删减this.executor.scheduleWithFixedDelay(new Runnable() {public void run() {try {checkConfigInfo();} catch (Throwable e) {LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", e);}}}, 1L, 10L, TimeUnit.MILLISECONDS);}
ClientWorker的构造方法创建了一个定时任务(每10ms运行一次),定时任务每次调用checkConfigInfo()方法:
public void checkConfigInfo() {// Dispatch taskes.//cacheMap里面存放的是CacheData对象,//当配置需要自动刷新时,会在cacheMap里面增加一条记录//cacheMap的key由groupId和dataId组成,value是CacheDataint listenerSize = cacheMap.size();// Round up the longingTaskCount.//将需要刷新的数据分组,每3000个为一组,一组由一个线程处理int longingTaskCount = (int) Math.ceil(listenerSize / ParamUtil.getPerTaskConfigSize());if (longingTaskCount > currentLongingTaskCount) {for (int i = (int) currentLongingTaskCount; i < longingTaskCount; i++) {// The task list is no order.So it maybe has issues when changing.executorService.execute(new LongPollingRunnable(i));}currentLongingTaskCount = longingTaskCount;}}
需要刷新的数据分好组之后,交给LongPollingRunnable类处理。
在介绍LongPollingRunnable之前,我们先回过头看一下NacosPropertySourcePostProcessor后处理器,该处理器专门处理注解@NacosPropertySource,该类提供了一个处理autoRefreshed的方法:
public static void addListenerIfAutoRefreshed(final NacosPropertySource nacosPropertySource, final Properties properties,final ConfigurableEnvironment environment) {//如果设置不自动刷新,直接返回if (!nacosPropertySource.isAutoRefreshed()) { // Disable Auto-Refreshedreturn;}//代码删减try {ConfigService configService = nacosServiceFactory.createConfigService(properties);//创建监听器Listener listener = new AbstractListener() {public void receiveConfigInfo(String config) {String name = nacosPropertySource.getName();NacosPropertySource newNacosPropertySource = new NacosPropertySource(dataId, groupId, name, config, type);newNacosPropertySource.copy(nacosPropertySource);MutablePropertySources propertySources = environment.getPropertySources();propertySources.replace(name, newNacosPropertySource);}};//添加监听器if (configService instanceof EventPublishingConfigService) {((EventPublishingConfigService) configService).addListener(dataId,groupId, type, listener);}else {configService.addListener(dataId, groupId, listener);}}//代码删减}
addListenerIfAutoRefreshed()方法的最后调用了configService.addListener()方法,而configService.addListener()方法最终又会调用ClientWorker.addTenantListeners():
public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners)throws NacosException {group = null2defaultGroup(group);String tenant = agent.getTenant();//创建CacheDataCacheData cache = addCacheDataIfAbsent(dataId, group, tenant);for (Listener listener : listeners) {//将监听器添加到CacheData中,当数据发生变化时,CacheData会通知这些监听器cache.addListener(listener);}}public CacheData addCacheDataIfAbsent(String dataId, String group, String tenant) throws NacosException {String key = GroupKey.getKeyTenant(dataId, group, tenant);CacheData cacheData = cacheMap.get(key);if (cacheData != null) {return cacheData;}cacheData = new CacheData(configFilterChainManager, agent.getName(), dataId, group, tenant);//cacheMap的key是由dataId, group, tenant三个参数组成的CacheData lastCacheData = cacheMap.putIfAbsent(key, cacheData);if (lastCacheData == null) {if (enableRemoteSyncConfig) {//访问服务端,从服务端拉取dataId, group对应的配置数据String[] ct = getServerConfig(dataId, group, tenant, 3000L);cacheData.setContent(ct[0]);//配置内容保存到CacheData中}//对当前的CacheData对象分组int taskId = cacheMap.size() / (int) ParamUtil.getPerTaskConfigSize();cacheData.setTaskId(taskId);lastCacheData = cacheData;}//代码删除return lastCacheData;}
通过addCacheDataIfAbsent()方法可以清晰的看到CacheData如何被创建以及保存了哪些数据,而且CacheData的分组规则与LongPollingRunnable的分组规则一样。
下面继续分析LongPollingRunnable类。LongPollingRunnable实现了Runnable接口,下面我们重点分析其run()方法。
public void run() {List<CacheData> cacheDatas = new ArrayList<CacheData>();List<String> inInitializingCacheList = new ArrayList<String>();try {//代码删减//从服务器上批量拉取本组内的配置发生变化的groupId和dataIdList<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);//遍历发生变化的配置for (String groupKey : changedGroupKeys) {String[] key = GroupKey.parseKey(groupKey);String dataId = key[0];String group = key[1];String tenant = null;if (key.length == 3) {tenant = key[2];}try {//从服务器拉取发生变化的配置数据String[] ct = getServerConfig(dataId, group, tenant, 3000L);CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));cache.setContent(ct[0]);//更新if (null != ct[1]) {cache.setType(ct[1]);}} catch (NacosException ioe) {//代码删减}}for (CacheData cacheData : cacheDatas) {if (!cacheData.isInitializing() || inInitializingCacheList.contains(GroupKey.getKeyTenant(cacheData.dataId, cacheData.group, cacheData.tenant))) {//通知CacheData的监听器//监听器的作用有://1、更改对象的属性值//2、替换spring容器的Environment里面的PropertySourcecacheData.checkListenerMd5();cacheData.setInitializing(false);}}inInitializingCacheList.clear();executorService.execute(this);//直接开始运行下次任务} catch (Throwable e) {//代码删减}}
从上面的介绍可以看到,nacos配置自动更新依靠的是两个定时任务,第一个定时任务是检查是否有新的需要自动刷新的配置;第二个定时任务是不断访问服务端检查是否有新的配置更新。
@NacosValue自动刷新原理
在NacosValue中也有一个autoRefreshed = true的配置,这个配置起什么作用,它和NacosPropertySource之间是什么关系?要回答这些问题,先看一下nacos如何处理该注解。
nacos提供了NacosValueAnnotationBeanPostProcessor后处理器处理注解NacosValue,并且提供了doWithAnnotation()方法处理autoRefreshed ,下面看一下该方法源码:
private void doWithAnnotation(String beanName, Object bean, NacosValue annotation,int modifiers, Method method, Field field) {if (annotation != null) {if (Modifier.isStatic(modifiers)) {return;}//判断是否是自动刷新if (annotation.autoRefreshed()) {String placeholder = resolvePlaceholder(annotation.value());if (placeholder == null) {return;}NacosValueTarget nacosValueTarget = new NacosValueTarget(bean, beanName,method, field, annotation.value());//如果属性需自动刷新,那么将配置名字和nacosValueTarget放到placeholderNacosValueTargetMap中,//placeholderNacosValueTargetMap是Map类型,其key为String,value为List<NacosValueTarget>put2ListMap(placeholderNacosValueTargetMap, placeholder,nacosValueTarget);}}}
doWithAnnotation()将需要刷新的对象和属性放到了一个Map中。
如果我们看一下NacosValueAnnotationBeanPostProcessor处理器的定义,会发现该类实现了ApplicationListener<NacosConfigReceivedEvent>,这说明该处理器还监听了NacosConfigReceivedEvent事件,而从服务器拉取了更新的配置数据后,通知CacheData的监听器时也会发布NacosConfigReceivedEvent,所以当服务器有更新的配置时,就会通知NacosValueAnnotationBeanPostProcessor。下面在来看一下该类的onApplicationEvent方法:
public void onApplicationEvent(NacosConfigReceivedEvent event) {//遍历需要自动刷新的属性for (Map.Entry<String, List<NacosValueTarget>> entry : placeholderNacosValueTargetMap.entrySet()) {String key = environment.resolvePlaceholders(entry.getKey());String newValue = environment.getProperty(key);if (newValue == null) {continue;}List<NacosValueTarget> beanPropertyList = entry.getValue();for (NacosValueTarget target : beanPropertyList) {String md5String = MD5Utils.md5Hex(newValue, "UTF-8");boolean isUpdate = !target.lastMD5.equals(md5String);if (isUpdate) {target.updateLastMD5(md5String);Object evaluatedValue = resolveNotifyValue(target.nacosValueExpr, key, newValue);if (target.method == null) {setField(target, evaluatedValue);//更新属性值}else {setMethod(target, evaluatedValue);//调用方法更新}}}}}
在onApplicationEvent()方法中,可以清晰的看到刷新配置的逻辑。
