vlambda博客
学习文章列表

nacos动态刷新原理

Nacos是什么

Nacos 致力于发现、配置和管理微服务。它提供了一组简单易用的特性集,帮助快速实现动态服务发现、服务配置、服务元数据及流量管理。使用Nacos 可以更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

服务(Service)是 Nacos 世界的一等公民。Nacos 支持几乎所有主流类型的“服务”的发现、配置和管理。

Nacos 的关键特性包括:

  • 服务发现和服务健康监测 ,支持基于DNS和RPC的服务发现,支持基于传输层和应用层的监控检查;

  • 动态配置服务 ,可以以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置,同时支持版本追踪和一键回滚等功能

  • 动态 DNS 服务 ,动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务;

  • 服务及其元数据管理 ,管理数据中心的所有服务及元数据,包括管理服务的描述、生命周期、服务的静态依赖分析、服务的健康状态、服务的流量管理、路由及安全策略、服务的 SLA 以及最首要的 metrics 统计数据。

基本架构

nacos动态刷新原理

逻辑架构

nacos动态刷新原理

NACOS动态配置

方式一:启动配置管理方式获取配置

1、项目引入POM包

<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2.2.2.RELEASE</version></dependency>

nacos动态刷新原理

2、yml配置NACOS系统信息

新增bootstrap.yml文件,配置信息写在该文件里。(问题:如放在application.yml会导致项目启动报找不到配置属性错误,原因:application.yml与bootstrap.yml加载顺序优先级问题。)

bootstrap.ymlbootstrap.properties)用来程序引导时执行,应用于更加早期配置信息读取,如可以使用来配置application.yml中使用到参数等
application.ymlapplication.properties) 应用程序特有配置信息,可以用来配置后续各个模块中需使用的公共参数等。
加载顺序:bootstrap.yml > application.yml > application-dev(prod).yml > ..

在bootstrap.yml中新增application.name和nacos的config信息。

spring: application: name: order-service-demo  cloud: nacos: config: server-addr: localhost:8848 file-extension: properties # 此处为配置使用的后缀名 group: DEFAULT_GROUP

nacos动态刷新原理

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 类型。

nacos动态刷新原理


4、代码配置

在使用配置的controller中新增 @RefreshScope 注解,以及在注入属性上新增 @Value("${Key名称}") 注解。访问网站处呈现

nacos动态刷新原理

方式二:JAVA SDK方式获取配置

1、项目引入POM包

<dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>2.0.0-ALPHA.2</version></dependency>

nacos动态刷新原理

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 类型。

nacos动态刷新原理


3、代码段

获取NACOS配置服务,根据Data ID获取配置。

nacos动态刷新原理

获取到的配置信息,接口返回呈现。

nacos动态刷新原理

为什么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() { @Override 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是CacheData int 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-Refreshed return; }
//代码删减 try {
ConfigService configService = nacosServiceFactory .createConfigService(properties); //创建监听器 Listener listener = new AbstractListener() {
@Override 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(); //创建CacheData CacheData 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和dataId List<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里面的PropertySource cacheData.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()方法中,可以清晰的看到刷新配置的逻辑。