«

Nacos源码学习计划-Day16-配置中心-加载远程配置源码解析

ZealSinger 发布于 阅读:65 技术文档


在上一节中,我们看到了Nacos读取配置中心内容的相关代码locate方法部分内容如下

// 加载共享配置文件
loadSharedConfiguration(composite);
// 加载额外配置文件
loadExtConfiguration(composite);
// 加载自身应用配置文件
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

那么接下来我们就进行一定的分析。

配置文件类型使用讲解

自身应用配置

Nacos配置中心的功能中,允许我们进行多样化的配置管理,对于不同的业务要求都能进行灵活的配置,我们可以来使用看看。

我们在代码中使用如下配置

spring:
application:
  name: stock-service
cloud:
  nacos:
     # 配置中心
    config:
      server-addr: http://xxxxx:8848

这个是使用最简单的配置,只需要配置地址即可,在读取 Nacos 配置中心文件时,是通过微服务名称去加载的,所以我们只需要在 Nacos 后台创建一个 stock-service 配置文件就可以读取到

image-20251116144921093

我们还可以指定文件类型(后缀)

spring:
application:
  name: stock-service
cloud:
  nacos:
     # 配置中心
    config:
      server-addr: http://xxxxx:8848
       # 配置文件类型
      file-extension: yaml

加上文件类型之后,我们在Nacos中再次创建一个新的配置,原来那个不删除,就会发现优先使用带尾缀的配置文件

image-20251116145113730

我们还可以指定环境,毕竟在正式开发中,我们肯定开发环境需要一套配置,测试环境也会需要一套配置,这个时候就能进行环境的区分

spring:
application:
  name: stock-service
profiles:
   # 测试环境
  active: test
cloud:
  nacos:
     # 配置中心
    config:
      server-addr: http://xxxxx:8848
       # 配置文件类型
      file-extension: yaml

这个时候在Nacos又新增一个带环境变量的配置文件,可以发现优先级更加高

image-20251116152922070

到这里我们先总结一下,在同一个的namespace下,如刚刚上文所说会有三种情况来读取配置文件,并且存在优先级关系默认的读取stock-service的配置文件优先级最低,这种是通过微服务名称去获取的,其次就是加上配置文件类型之后,会去读取指定文件类型的配置,比不加文件类型的配置文件优先级要高,最后我们通过指定项目环境,发现这种方式优先级是最高的

共享配置

实际开发中,我们因为设备环境有限,可能会出现部分业务系统公用一个数据库,同一个中间件即共享配置

在Nacos中我们也可以实现共享配置的功能

spring:
application:
  name: stock-service
profiles:
   # 测试环境
  active: test
cloud:
  nacos:
     # 配置中心
    config:
      server-addr: http://xxxxx:8848
       # 配置文件类型
      file-extension: yaml
       # 共享配置文件
      shared-configs:
        - dataId: common-mysql.yaml
          group: DEFAULT_GROUP
           # 中间件配置一般不需要刷新
          refresh: false

在 Nacos config 配置下可以指定shared-configs配置,这里是个数组类型,就表示可以配置多个共享配置文件,就可以把一些中间件配置管理起来。但是这里要注意的是,不要和自身应用配置文件配置重复,这样是不会生效的,因为自身应用配置文件共享配置文件优先级要高(后续源码分析的时候会看到,上一篇讲的源码中也能看出来,共享配置是最先被加载,也就导致共享配置优先级是最低的),如下图:

image-20251116153254694

当然,如果上述的自身应用配置和共享配置不能满足需求,还可以使用额外配置/扩展配置文件(extension-configs)

远程配置文件加载顺序源码分析

还是回到代码上来,上一篇中的提到的locate()方法

// 加载共享配置文件
loadSharedConfiguration(composite);
// 加载额外配置文件
loadExtConfiguration(composite);
// 加载自身应用配置文件
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);

首先从这里我们就可以知道加载优先级和配置优先级,对于Nacos而言,配置其实就是key-value的形式的数据存储且对于一些配置有且只能由一个,那么很明显,在不同的配置文件中如果出现了相同的配置项,自然是优先加载的权重越低,因为后加载的可以覆盖前面加载的

加载优先级
自身应用配置文件 < 额外配置文件 < 共享配置文件

配置优先级
自身应用配置文件 > 额外配置文件 > 共享配置文件

我们再来看看 loadApplicationConfiguration() 方法 ,加载自身应用配置文件的优先级源码如下:

private void loadApplicationConfiguration(
     CompositePropertySource compositePropertySource, String dataIdPrefix,
     NacosConfigProperties properties, Environment environment) {
  String fileExtension = properties.getFileExtension();
  String nacosGroup = properties.getGroup();
  // 加载 微服务名 配置文件
  loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
        fileExtension, true);
       
  // 加载 微服务.后缀名 配置文件
  loadNacosDataIfPresent(compositePropertySource,
        dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
       
  // 加载 微服务-环境变量名.后缀名 ,因为环境变量可以配置多个,所以这里是循环
  for (String profile : environment.getActiveProfiles()) {
     String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
     loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
           fileExtension, true);
  }

}

从上面代码来看以及我们上面截图中的使用情况来看,也是相匹配的。微服务-环境变量名.后缀名 这种形式的文件因为是最后加载,所以权重很大,会将前面的相同的配置项给覆盖

不管是获取什么类型的配置文件,最终都会走到 loadNacosDataIfPresent() 这个方法,而在这个方法里面最终会通过 HTTP 的方式去获取 Nacos 服务端的配置文件数据,请求的地址是:/v1/cs/configs,获取到数据之后会立马持久化到本地数据,我们接下来看看这个接口对应的逻辑,如何进行的处理

远程配置文件读取源码分析

根据路径,找到对应的ConfigController代码如下,可以看到,核心的方法是最后一行的inner.doGetConfig

image-20251116201243968

我们点到这个方法内部去看,内容如下,注意大量代码来袭~,代码很多但是我们暂时不需要全部都看,直接看如下代码中注释的那个方法

/**
* Execute to get config API.
*/
public String doGetConfig(HttpServletRequest request, HttpServletResponse response, String dataId, String group,
       String tenant, String tag, String clientIp) throws IOException, ServletException {
   final String groupKey = GroupKey2.getKey(dataId, group, tenant);
   String autoTag = request.getHeader("Vipserver-Tag");
   String requestIpApp = RequestUtil.getAppName(request);
   int lockResult = tryConfigReadLock(groupKey);
   
   final String requestIp = RequestUtil.getRemoteIp(request);
   boolean isBeta = false;
   if (lockResult > 0) {
       FileInputStream fis = null;
       try {
           String md5 = Constants.NULL;
           long lastModified = 0L;
           CacheItem cacheItem = ConfigCacheService.getContentCache(groupKey);
           if (cacheItem != null) {
               if (cacheItem.isBeta()) {
                   if (cacheItem.getIps4Beta().contains(clientIp)) {
                       isBeta = true;
                  }
              }

               final String configType =
                      (null != cacheItem.getType()) ? cacheItem.getType() : FileTypeEnum.TEXT.getFileType();
               response.setHeader("Config-Type", configType);
               FileTypeEnum fileTypeEnum = FileTypeEnum.getFileTypeEnumByFileExtensionOrFileType(configType);
               String contentTypeHeader = fileTypeEnum.getContentType();
               response.setHeader(HttpHeaderConsts.CONTENT_TYPE, contentTypeHeader);
          }
           // file即配置文件
           File file = null;
           ConfigInfoBase configInfoBase = null;
           PrintWriter out = null;
           if (isBeta) {
               md5 = cacheItem.getMd54Beta();
               lastModified = cacheItem.getLastModifiedTs4Beta();
               if (PropertyUtil.isDirectRead()) {
                   configInfoBase = persistService.findConfigInfo4Beta(dataId, group, tenant);
              } else {
                   // 这里是第一次对于file进行复制,直接看这里即
                   file = DiskUtil.targetBetaFile(dataId, group, tenant);
              }
               response.setHeader("isBeta", "true");
              ........
}

targetBetaFile方法的源码如下,可以发现,其实就是从本地文件中去进行获取,而不是从数据库中,那么问题来了,服务器上的本地文件啥时候来的呢?

  public static File targetTagFile(String dataId, String group, String tenant, String tag) {
   File file = null;
   if (StringUtils.isBlank(tenant)) {
       file = new File(EnvUtil.getNacosHome(), TAG_DIR);
  } else {
       file = new File(EnvUtil.getNacosHome(), TENANT_TAG_DIR);
       file = new File(file, tenant);
  }
   file = new File(file, group);
   file = new File(file, dataId);
   file = new File(file, tag);
   return file;
}

这里就需要来说一下了,我们知道Nacos分为集群和单机模式,对于集群环境下,使用过的UU应该不陌生,集群环境下是必须要配置外部数据库的,其主要目的是为了方便集群中节点的一些中心化管理;那么在单机模式下,Nacos是不是就是0数据库存储依赖,完全依靠本地呢?答案是NO,在Nacos单机模式下,引入了Derby这个内嵌式数据库进行存储

对于集群环境下,大家应该比较好理解,一个节点Nacos重新进入集群,同步数据的时候,其实就会将数据保存到本地,集群中的节点本地磁盘上会出现配置信息很好理解,这个过程肯定是在Nacos启动的时候就会进行;那么对于Nacos单机环境下,其实也是一样的,大家可以自己找一下,定位到 DumpAllProcessor类中的process方法,在这个方法中主要干了两件事:先是去通过分页查询数据库中的config_info表数据,查询到数据之后,最终持久化到本地磁盘,其代码内容如下

 // 分页查询配置信息
Page<ConfigInfoWrapper> page = persistService.findAllConfigInfoFragment(lastMaxId, PAGE_SIZE);


// 查询数据库
@Override
public Page<ConfigInfoWrapper> findAllConfigInfoFragment(final long lastMaxId, final int pageSize) {
   String select = "SELECT id,data_id,group_id,tenant_id,app_name,content,md5,gmt_modified,type from config_info where id > ? order by id asc limit ?,?";
   PaginationHelper<ConfigInfoWrapper> helper = createPaginationHelper();
   try {
       return helper.fetchPageLimit(select, new Object[] {lastMaxId, 0, pageSize}, 1, pageSize,
               CONFIG_INFO_WRAPPER_ROW_MAPPER);
  } catch (CannotGetJdbcConnectionException e) {
       LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e);
       throw e;
  }
}

上述的fetchPageLimit是接口PaginationHelper中定义的方法,PaginationHelper的两个实现EmbeddedPaginationHelperImpl和ExternalStoragePaginationHelperImply都是进行分页的,只是两者的分页逻辑不太一样,前者的内部是DatabaseOperate进行的数据库操作,也就是Derby这个数据库的操作类;后者使用的JdbcTemplate,对于JDBC大家肯定不陌生了,兼容多种第三方数据库

继续回到上述的process方法,读取完数据库后的操作如下,会进行写入本地磁盘的操作

// 把查询到配置写入到磁盘
boolean result = ConfigCacheService
      .dump(cf.getDataId(), cf.getGroup(), cf.getTenant(), cf.getContent(), cf.getLastModified(),
               cf.getType());
               

// 在 dump 方法中,会调用持久化到本地磁盘方法
DiskUtil.saveToDisk(dataId, group, tenant, content);

看到这里,我们 Nacos 服务端获取配置数据整个流程就串起来了。简单地说,就是客户端查询配置数据,服务端是直接获取的本地磁盘文件中的配置,而磁盘文件上的配置数据,是在服务端启动的时候会去查询数据库数据,然后持久化到磁盘上。

总结

本章主要讲解了在 Nacos 配置中心不同类型的配置文件的使用方式,尤其重要的是它们之间的优先级关系,并且分析了它们的源码实现。随后我们还分析了客户端在查询配置文件的时候,服务端是怎么处理的,主要就是读取本地磁盘文件。

大家也可以手动试一试,直接手动修改数据库中的配置信息,客户端会不会生效?这下知道了吧,并不是直接读取的数据库,所以自然不会立马生效。

还有一点就是,直接修改数据库是不会触发通知客户端进行变更事件的,在下一个章节中,我再来讲一讲客户端它是怎么感知配置文件变更的。

 

编程 Java 项目