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-demo
cloud:
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是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() {
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()方法中,可以清晰的看到刷新配置的逻辑。