Nacos配置中心使用以及源码分析


Nacos不仅仅只是用于做注册中心,它还可以做配置中心使用。那么什么是配置中心呢?配置中心就是为分布式系统提供统一的外部配置服务端。

简单使用

引入依赖

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

添加bootstrap.yml

server:
  port: 7777
spring:
  application:
    name: user-service
  profiles:
    active: dev
  cloud:
      config:
        server-addr: localhost:8848
        file-extension: yml
        shared-configs:
          - dataId: common-db.yml
            group: "PROJECTDEMO"
            refresh: true
        extension-configs:
          - dataId: ext-config.yml
            group: "PROJECTDEMO"
            refresh: true

启动服务即可使用配置中心了,它会将配置中心的配置引入项目

202111141636867094590

加载顺序

项目引入配置不仅仅只是引入已配的dataId,还会根据服务名,服务环境加载一些配置,比如我上面的服务是user-service环境是dev

那么他还会加载user-service-dev.yml、user-service.yml、user-service加上配置的共享配置common-db.yml,扩展配置ext-config.yml

202111141636867690190

那么这5种配置优先级是怎样的呢?从高到低

  1. user-service-dev.yml 同工程同环境
  2. user-service.yml 同工程不同环境
  3. user-service 同工程不同环境无扩展后缀
  4. ext-config.yml 扩展配置
  5. common-db.yml 共享配置

配置动态感知

配置现在可以统一管理,那么如果客户端可以动态感知到配置变化更新存储的配置Bean多好呢,Nacos也是支持这种功能的,

使用配置类来创建bean时,若要实现注入bean的刷新,需要在配置类和Bean创建方法上均加上@RefreshScope注解。在对应配置被修改后,所有开启了刷新的注入bean在下一次调用时会重新进行初始化并替换掉之前注入的Bean。

注意点,即使使用的是final修饰对象,配置更改也回导致对象更改,原因是@RefreshScope底层使用的是cglib动态创建子类来实现的

加载顺序源码分析

首先同样我们来到spring-cloud-starter-alibaba-nacos-config这个包下面的spring.factories,最终我们定位到NacosConfigBootstrapConfiguration

从NacosConfigBootstrapConfiguration分析得出加载nacos配置中心配置是NacosPropertySourceLocator(所有注入的bean都在这个类里面),在locate方法里面调用了配置的加载。至于为什么是这个方法那就要从springboot启动类开始

SpringApplication#run  (spring-boot)
--prepareContext
----applyInitializers
------PropertySourceBootstrapConfiguration#initialize  (spring-cloud)
--------locateCollection
----------locate (spring-cloud-nacos-config)
@Override
public PropertySource<?> locate(Environment env) {
   nacosConfigProperties.setEnvironment(env);
   ConfigService configService = nacosConfigManager.getConfigService();

   if (null == configService) {
      log.warn("no instance of config service found, can't load config from nacos");
      return null;
   }
   long timeout = nacosConfigProperties.getTimeout();
   nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
         timeout);
   String name = nacosConfigProperties.getName();

   String dataIdPrefix = nacosConfigProperties.getPrefix();
   if (StringUtils.isEmpty(dataIdPrefix)) {
      dataIdPrefix = name;
   }

   if (StringUtils.isEmpty(dataIdPrefix)) {
      dataIdPrefix = env.getProperty("spring.application.name");
   }

   CompositePropertySource composite = new CompositePropertySource(
         NACOS_PROPERTY_SOURCE_NAME);
     // 加载共享配置
   loadSharedConfiguration(composite);
   // 加载扩展配置
   loadExtConfiguration(composite);
   // 加载内部规则配置
   loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

   return composite;
}

从这段代码基本可以分析共享配置优先级<扩展配置<内部规则配置

但内部规则的这些配置的加载顺序还没有发现

loadApplicationConfiguration

private void loadApplicationConfiguration(
      CompositePropertySource compositePropertySource, String dataIdPrefix,
      NacosConfigProperties properties, Environment environment) {
   String fileExtension = properties.getFileExtension();
   String nacosGroup = properties.getGroup();
   // load directly once by default
   // 加载{service-name}
   loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
         fileExtension, true);
   // 加载{service-name}.{extension}
   loadNacosDataIfPresent(compositePropertySource,
         dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
   // 加载{service-name}-{environment}.{extension}
   for (String profile : environment.getActiveProfiles()) {
      String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
      loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
            fileExtension, true);
   }

}

从这里我们又可以发现内部规则的加载顺序

优先级:{service-name}-{environment}.{extension}->{service-name}.{extension}->{service-name}

综上优先级得出

  1. service-name}-{environment}.{extension}
  2. {service-name}.{extension}
  3. {service-name}
  4. 扩展配置
  5. 共享配置

至于怎么加载的就不详细展开了,最后是通过调用api去获取配置的

202111141636871772404

配置动态感知

首先同样我们来到spring-cloud-starter-alibaba-nacos-config这个包下面的spring.factories,最终我们定位到NacosConfigAutoConfiguration

同样我们来到NacosContextRefresher我们发现它实现了一个事件监听ApplicationListener<ApplicationReadyEvent>

@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
   // many Spring context
   if (this.ready.compareAndSet(false, true)) {
      this.registerNacosListenersForApplications();
   }
}

private void registerNacosListenersForApplications() {
    // 是否刷新配置,默认FALSE需要配置就是我们配置的refresh: true
        if (isRefreshEnabled()) {
            for (NacosPropertySource propertySource : NacosPropertySourceRepository
                    .getAll()) {
                if (!propertySource.isRefreshable()) {
                    continue;
                }
                String dataId = propertySource.getDataId();
        // 为每个dataId注册一个Nacos监听器
                registerNacosListener(propertySource.getGroup(), dataId);
            }
        }
    }

registerNacosListener

注册一个监听器,监听nacos配置中心是否变动并更新配置

private void registerNacosListener(final String groupKey, final String dataKey) {
   String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
   Listener listener = listenerMap.computeIfAbsent(key,
         lst -> new AbstractSharedListener() {
            @Override
            public void innerReceive(String dataId, String group,
                  String configInfo) {
               // 更新次数记录
               refreshCountIncrement();
               // 添加一条记录--作为历史版本可查
               nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
               // 发布一个事件,事件监听器RefreshEventListener
               applicationContext.publishEvent(
                     new RefreshEvent(this, null, "Refresh Nacos config"));
            }
         });
      // 监听器注册 发布配置后回调innerReceive方法
      configService.addListener(dataKey, groupKey, listener);
   
}

RefreshEventListener

监听RefreshEvent事件,说明有配置发布了

@Override
public void onApplicationEvent(ApplicationEvent event) {
   if (event instanceof ApplicationReadyEvent) {
      handle((ApplicationReadyEvent) event);
   }
   else if (event instanceof RefreshEvent) {
      handle((RefreshEvent) event);
   }
}

public void handle(RefreshEvent event) {
        if (this.ready.get()) { 
            Set<String> keys = this.refresh.refresh();
        }
    }

this.refresh.refresh

public synchronized Set<String> refresh() {
   Set<String> keys = refreshEnvironment();
   this.scope.refreshAll();
   return keys;
}

public synchronized Set<String> refreshEnvironment() {
  // 将除了系统级别变量提取出来,为了后续更新
  /* 系统级别变量: StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
                    StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
                    StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME,
                    StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME,
                    StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME,*/
  Map<String, Object> before = extract(
    this.context.getEnvironment().getPropertySources());
  // 将配置放到环境变量里面去
  addConfigFilesToEnvironment();
  // 比对并更新
  Set<String> keys = changes(before,
                             extract(this.context.getEnvironment().getPropertySources())).keySet();
  // 发布一个环境变更事件
  this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
  return keys;
}

public void refreshAll() {
  // 将原来的配置缓存清除
  super.destroy();
  // 发布事件
  this.context.publishEvent(new RefreshScopeRefreshedEvent());
}

配置文件Dump

服务端启动时从数据库中加载配置到本地磁盘上。服务端会根据心跳文件中保存的最后一次心跳时间,来判断全量dump还是增量dump配置数据(由心跳间隔6小时判断)。

全量 dump :先清空磁盘缓存,然后根据主键 ID 每次提取一千条配置刷进磁盘和内存

增量 dump :提取最近6小时的新增配置(包括更新的和删除的),先按照这批数据刷新一遍内存和文件,再根据内存里所有的数据全量去 比对一遍数据库,如果有改变的再同步一次,相比于全量dump减少一定的数据库IO和磁盘IO次数


文章作者: dm
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 dm !
评论
 上一篇
Dubbo高级应用 Dubbo高级应用
前言Dubbo作为一个强大的RPC框架,他的功能和特性很多。这里针对其中使用比较多的功能和特性进行测试和记录。 负载均衡官网地址:http://dubbo.apache.org/zh/docs/v2.7/user/examples/load
2023-05-01
下一篇 
Nacos服务注册源码分析 Nacos服务注册源码分析
源码构建版本:1.4.1 源码地址 git 下载git clone -b 1.4.1 https://github.com/alibaba/nacos.git maven编译mvn -Prelease-nacos -Dmaven.test
2023-03-28
  目录