Nacos 配置中心原理解析

Nacos 配置中心的主要作用

本来我寻思把这篇文章放到负载均衡那个讲的,结构感觉那样会很长,我感觉没有人想读论文那样长的技术博客,就算了

在分布式系统中,配置管理面临三大核心挑战:配置分散(多服务、多环境配置散落在各节点)、更新繁琐(修改配置需重启服务)、一致性难保证(多节点配置易出现不一致)。Nacos 配置中心的核心作用就是解决这些问题

  1. 配置集中化管理 将所有服务的配置(如数据库连接、接口地址、限流规则等)集中存储在 Nacos Server,替代传统的本地配置文件(如 application.yml),避免配置分散在各服务节点的文件系统中,便于统一维护。
  2. 动态配置更新 支持配置在运行时动态修改并生效,无需重启服务。例如:修改数据库连接池参数、调整日志级别、更新限流阈值等,通过 Nacos 推送机制实时同步到所有相关服务,极大提升系统灵活性。
  3. 多维度配置隔离 通过「命名空间(Namespace)+ 分组(Group)+ Data ID」三级结构,实现多环境(如 dev/test/prod)、多业务(如支付 / 订单)、多版本配置的隔离,避免配置混乱。
  4. 配置高可用保障 基于集群部署和数据持久化,确保配置不丢失且服务稳定。即使部分 Nacos 节点故障,仍能通过集群同步机制保证配置可用;客户端本地缓存机制也能在 Nacos 服务不可用时,保障应用正常启动。
  5. 配置版本管理与回溯 记录配置的历史修改记录,支持版本回滚。当配置变更引发问题时,可快速回退到之前的稳定版本,降低变更风险。

所以 Nacos 作为配置中心,起到微服务架构中配置管理的核心组件,其核心价值在于解决分布式系统中配置的动态化、集中化、高可用管理问题。

说白了,Nacos Config 针对配置的管理提供了5种操作:

  • 获取配置,从Nacos Config Server中读取配置。
  • 监听配置:订阅感兴趣的配置,当配置发生变化的时候可以收到一个事件。
  • 发布配置:将配置保存到Nacos Config Server中。
  • 动态更新配置:当配置发生变化的时候可以及时变更配置
  • 删除配置:删除配置中心的指定配置。

而从原理层面来看,可以归类为两种类型:配置的CRUD和配置的动态监听。

Nacos 配置中心的实现原理

配置的管理,存储和发布

对于Nacos Config来说,主要是提供了配置的集中式管理功能,然后对外提供CRUD的访问接口使得应用系统可以完成配置的基本操作。

  • 对于服务端来说:需要考虑的是配置如何存储,是否需要持久化。
  • 对于客户端来说:需要考虑的是通过接口从服务器查询得到相应的数据然后返回。

之前说过Nacos的服务发现与注册都是CS架构,而Nacos 配置中心也采用 客户端 - 服务端 架构:

  • 服务端(Nacos Server):负责配置的存储、管理、变更通知和集群同步,可集群部署。
  • 客户端(Nacos Client):嵌入到应用中,负责拉取配置、监听配置变更、更新本地配置

关系如下

image-20250721145557912

你新建了或者修改了配置,还没有通知到其他服务的之前的时候,需要经过Nacos的配置管理然后再进行配置发布

配置发布的流程大致如下:(客户端 / 控制台 → Nacos Server)

  • 操作发起:

    用户(一般服务不会主动更改配置)通过 Nacos 控制台、API 或 SDK 向 Nacos Server 发布配置(如 Data IDGroup配置内容配置格式等)。

    • Data ID:配置的唯一标识,通常以「应用名 + 环境 + 文件后缀」命名(如 user-service-dev.yaml)。
  • 服务端持久化:

    Nacos Server 接收配置后,将其存储到本地文件系统和数据库(默认嵌入式数据库 Derby,可配置 MySQL 实现持久化,这也就是为什么 Nacos 在安装之后要打开配置文件编辑子节点mysql),确保服务再关闭的时候,配置相关的数据不会丢失。

    • 本地文件系统:作为缓存,路径为 nacos/data/config-data/{namespace}/{group}/{dataId},用于快速读取。
    • 数据库:用于持久化存储,支持集群环境下的配置同步。
    • 集群数据的同步涉及到 Raft 协议来实现集群数据一致性。Leader 节点负责处理配置更新请求,Follower 节点通过同步 Leader 的日志实现数据一致,确保集群中所有节点的配置状态相同。这里只简单说一下。

配置的获取(客户端 → Nacos Server)

image-20250722232240556

上面说了,对于客户端来说,需要考虑的是通过接口从服务器查询得到相应的数据然后返回。

配置被发布,此时服务端已经保存下来了之前用户发布到服务端的配置,所以在服务启动时,客户端(Nacos SDK)会向 Nacos Server 拉取指定的配置,流程如下:

  • 客户端初始化:应用启动时,通过 Nacos SDK 配置 Data IDGroupServer 地址等信息。
  • 拉取配置请求:客户端向 Nacos Server 发送 HTTP 请求,携带 Data IDGroup配置的 MD5 哈希值(首次拉取时为 null)。
  • 服务端响应:Nacos Server 根据 Data ID 和 Group 查询配置:
    • 若为首次拉取(无 MD5),直接返回完整配置内容和对应的 MD5。
    • 若已有 MD5,对比服务端配置的 MD5:若一致,返回空(无需更新);若不一致,返回新配置和新 MD5。
  • 客户端加载:客户端将拉取的配置加载到内存(如 Spring 应用会将配置注入 Environment),供业务逻辑使用。

配置的动态监听

大家知道上面其实都是 Nacos 的普通功能,其中 Nacos 有一个功能就是能够在你运行的时候更改配置,对应涉及到的服务就会监听到然后自动修改,Nacos 学长这招太狠了

这种配置的动态监听就是 Nacos 的客户端和服务端之间存在的数据交互的一种行为(不然怎么做到实时的更新和数据的查询呢),而对于这种交互行为共有两种方式:

  • Pull模式:表示客户端从服务端主动拉取数据。
    • Pull模式下,客户端需要定时从服务端拉取一次数据,由于定时带来的时间间隔,因此不能保证数据的实时性并且在服务端配置长时间不更新的情况下,客户端的定时任务会做一些无效的Pull操作。
  • Push模式:服务端主动把数据推送到客户端
    • Push模式下,服务端需要维持与客户端的长连接,并且为了检测连接的有效性,还需要心跳机制来维持每个连接的状态。

Nacos采用的是 Pull 模式(Kafka也是如此),并且采用了一种长轮询机制,这招长轮询机制是指客户端发起轮询请求后,服务端如果有配置发生变更,就直接返回。客户端采用长轮询的方式定时的发起 Pull 请求,去检查服务端配置信息是否发生了变更,如果发生了变更,那么客户端会根据变更的数据获得最新的配置。

也就是说,长轮询就是在客户端发起Pull请求后,如果发现服务端的配置和客户端的配置是保持一致的,那么服务端会一直保持住这个请求。(服务端拿到这个连接后在指定的时间段内不会返回结果,直到这段时间内的配置发生变化),一旦配置发生了变化,服务端会把原来保持住的请求进行返回。

image-20250721151104538

所以 Nacos 为实现配置动态更新,客户端会与 Nacos Server 建立长连接监听机制,避免频繁轮询,流程如下:

  • 建立长连接

    配置拉取成功后,客户端通过 HTTP 长轮询 或者 2.x 新版本天生支持的 gRPC 协议来进行长轮询维持,用来向 Nacos Server 注册监听,请求中会携带需监听的 Data IDGroup 和当前配置的 MD5

  • 服务端等待变更:Nacos Server 收到监听请求后,不会立即响应,会检查配置是否发生了变更,然后将请求放入一个阻塞队列,等待配置发生变更:

    • 若在 30 秒内(默认超时时间,可通过 nacos.config.long-poll.timeout 配置)配置发生变更,那么触发一个事件机制,监听到该事件的任务会遍历 allSubs 队列,找到发生变更的配置项对应的 ClientLongPolling 任务,将变更的数据通过该任务中的连接进行返回,即完成了一次推送操作。也就是此时服务端会立即通知客户端并且返回变更的配置信息。

    • 若 30 秒内无变更,服务端返回空响应,客户端收到后重新发起长轮询(维持监听)。

  • 客户端处理变更:当客户端收到配置变更通知后,会重新拉取最新配置,并触发本地配置更新逻辑(如 Spring 的 @RefreshScope 注解可实现 Bean 动态刷新)。

image-20250721151227624

配置的动态更新

之后,动态监听到配置一旦发生到更改,会带着新的配置来返回,此时就会涉及到一个配置点动态更新,Nacos Server 会经历「合法性校验→数据持久化→变更记录→MD5 生成」的完整流程

  • 更新触发:用户在控制台修改配置并发布,请求会携带 Data IDGroupNamespace、新配置内容等参数发送到 Nacos Server。Nacos Server 接收更新请求,更新本地文件和数据库中的配置,并生成新的 MD5。
    • 服务端首先进行合法性校验,然后进行数据的持久化和缓存的更新,然后生成新的 MD5 与变更的通知标记,这样,一条完整的轮回,就此开始与结束
  • 通知监听客户端:Nacos Server 遍历阻塞队列中监听该配置的客户端,主动唤醒对应的长轮询请求,返回新配置的 MD5 或内容。
    • 客户端启动时,会通过 addListener 方法向 Nacos Server 注册配置监听,携带参数
    • 服务端的阻塞队列由专门的线程池(ConfigExecutor)管理,核心逻辑是等待配置变更,若变更则立即唤醒,否则超时唤醒,这里源码再说
    • 然后如果是集群,集群用 Raft 协议同步一下
  • 客户端同步更新:客户端收到通知后,重新拉取最新配置,覆盖本地缓存,并触发业务逻辑的配置刷新。
    • 客户端收到通知(包含新 MD5)后,会立即向 Nacos Server 发送「获取最新配置」的请求,携带 Data IDGroup 和新 MD5(用于服务端校验)。服务端返回完整的新配置内容,客户端接收后进行格式解析
    • 然后就是更新本地存储与缓存
    • 最后的最后,最重要的一点,这些新修改的配置如何生效呢,可能会这样
      • 出现事件监听:客户端发布配置变更事件(如 ConfigChangeEvent),业务代码通过注册监听器(onChange 方法)接收事件,执行自定义逻辑(如重新初始化连接池、更新缓存策略)。
      • 框架自动刷新:若集成 Spring Cloud,可通过 @RefreshScope 注解标记需要动态刷新的 Bean,框架会在配置变更时销毁旧 Bean 并重新创建,注入新配置。

从源码看 Nacos 配置中心的工作实现

Spring Boot 启动中自动配置的原理过渡到 Nacos

首先需要了解到,Spring Cloud 是基于 Spring Boot 来扩展的,而 Spring 本身就提供了Environment,用来表示 Spring 应用程序的环境配置(包括外部环境),并且提供了统一访问的方法getProperty(String key)来获取配置。

对于SpringCloud而言,要实现统一配置管理并且动态的刷新配置,需要解决两个问题:

  • 如何将远程服务器上的配置(Nacos Config Server)加载到Environment上。
  • 配置变更时,如何将新的配置更新到Environment中。

对于配置的加载而言,需要牵扯到SpringBoot的自动装配,进行环境的准备工作:

还记得之前,在 Spring Boot 讲解主类的源代码的时候,run方法会进行环境的准备工作,其中都是涉及到环境的准备,封装,和上下文的准备和刷新等内容

image-20250721153858167

重点来看this.prepareEnvironment(listeners, applicationArguments)这个方法:

1
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
image-20250721154036969

这是准备环境(prepareEnvironment)的逻辑,主要负责初始化和配置 ConfigurableEnvironment,并触发环境准备完成的事件。其中,主要会发布一个ApplicationEnvironmentPreparedEvent事件,通知所有监听器环境已准备好。然后将环境绑定到 SpringApplication 实例。而BootstrapApplicationListener监听器会监听这一类的事件,并作出响应的处理,这些内容都是在

监听事件后的处理,进行了自动装配:

image-20250721154706420
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 当 ApplicationEnvironmentPreparedEvent 事件触发时执行
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
// 获取当前环境配置
ConfigurableEnvironment environment = event.getEnvironment();

......

// 如果未找到上下文,则创建 Bootstrap 服务上下文
// 这里调用了bootstrapServiceContext()方法
if (context == null) {
context = this.bootstrapServiceContext(environment, event.getSpringApplication(), configName);
// 添加监听器,在失败时关闭上下文
event.getSpringApplication().addListeners(new ApplicationListener[]{new CloseContextOnFailureApplicationListener(context)});
}

// 将 Bootstrap 上下文应用到当前环境
this.apply(context, event.getSpringApplication(), environment);
}
}
}

bootstrapServiceContext()方法则实现了,自动装配BootstrapImportSelectorConfiguration并且完成了上下文的配置和准备工作

1
2
builder.sources(new Class[]{BootstrapImportSelectorConfiguration.class});
ConfigurableApplicationContext context = builder.run(new String[0]);

而自动装配类BootstrapImportSelectorConfiguration向 Spring 容器中导入BootstrapImportSelector,这个类在之前讲 Spring Boot 的源码中我们说过,它负责自动配置信息的加载。把 Nacos 各个部分的 spring.factories 注册了进去。

BootstrapImportSelector通常是一个实现了ImportSelector接口的类,ImportSelector接口的核心方法是selectImports ,它会返回需要导入到 Spring 容器中的类的全限定名数组。也就是说,BootstrapImportSelector会根据一定的条件和逻辑,决定哪些类应该被导入到 Spring 容器中,这些类往往是与应用启动初期的配置、引导等相关的,比如导入一些配置处理器、监听器等,以完善 Spring 应用的初始化配置。

1
2
3
4
5
6
7
8
9
10
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration(
proxyBeanMethods = false
)
@Import({BootstrapImportSelector.class})
public class BootstrapImportSelectorConfiguration {
}

在 Spring Cloud 应用启动时,需要先加载一些基础的配置,比如 Nacos 配置中心的地址、命名空间等信息。BootstrapImportSelector可能会在应用启动的早期阶段,负责导入与 Nacos 配置中心相关的配置类、处理器或者监听器。在我们配置中心这块,这些配置类就在如下位置

image-20250721155821892

其中的 spring.factroies 会自动装配如下类

image-20250721155628418

其中最重要的就是NacosConfigBootstrapConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.alibaba.cloud.nacos;

import com.alibaba.cloud.nacos.client.NacosPropertySourceLocator;
import com.alibaba.cloud.nacos.refresh.SmartConfigurationPropertiesRebinder;
import com.alibaba.cloud.nacos.refresh.condition.ConditionalOnNonDefaultBehavior;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.cloud.context.properties.ConfigurationPropertiesBeans;
import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
name = {"spring.cloud.nacos.config.enabled"},
matchIfMissing = true
)
public class NacosConfigBootstrapConfiguration {
@Bean
@ConditionalOnMissingBean
public NacosConfigProperties nacosConfigProperties() {
return new NacosConfigProperties();
}

@Bean
@ConditionalOnMissingBean
public NacosConfigManager nacosConfigManager(NacosConfigProperties nacosConfigProperties) {
return new NacosConfigManager(nacosConfigProperties);
}

@Bean
public NacosPropertySourceLocator nacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {
return new NacosPropertySourceLocator(nacosConfigManager);
}

@Bean
@ConditionalOnMissingBean(
search = SearchStrategy.CURRENT
)
@ConditionalOnNonDefaultBehavior
public ConfigurationPropertiesRebinder smartConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {
return new SmartConfigurationPropertiesRebinder(beans);
}
}

NacosConfigBootstrapConfiguration 也是 Nacos 配置中心在 Spring Cloud 环境中的核心启动配置类,主要作用是初始化 Nacos 配置中心的核心组件,其中

  • nacosConfigProperties()创建 NacosConfigProperties 实例,用于封装 Nacos 配置中心的核心配置参数,这些参数可以通过 Spring Boot 的配置文件(进行配置,NacosConfigProperties 会自动绑定这些配置。

  • 创建 NacosConfigManager 实例,它是 Nacos 配置中心的核心管理器,负责:

    • 初始化 Nacos 客户端(ConfigService),建立与 Nacos 服务器的连接。
    • 提供配置的获取、监听、发布等核心操作的入口。
    • 依赖 NacosConfigProperties 中的配置参数来初始化客户端,是后续所有配置操作的基础。
  • nacosPropertySourceLocator(NacosConfigManager)创建 NacosPropertySourceLocator 实例,它是 Spring Cloud 中配置源定位器的实现。

    • 作用是:在 Spring 容器启动阶段(bootstrap 阶段),通过 NacosConfigManager 从 Nacos 服务器拉取配置,并将这些配置封装为 Spring 的 PropertySource 加入到环境变量中。
    • 这样一来,应用中通过 @Value@ConfigurationProperties 注解就能直接获取 Nacos 中的配置,与本地配置无缝融合。
  • 最重要的是,smartConfigurationPropertiesRebinder(ConfigurationPropertiesBeans)创建 SmartConfigurationPropertiesRebinder 实例(继承自 Spring Cloud 的 ConfigurationPropertiesRebinder)。

    • 作用是:当 Nacos 配置中心的配置发生动态更新时,自动触发 Spring 中 @ConfigurationProperties 注解标记的 Bean 的重新绑定,确保这些 Bean 能及时获取最新配置,实现配置的动态刷新

这个配置类让大家窥探了Nacos 配置中心工作的基本流程,所以我就详细的说了一下

还有一个这个类也会被自动加载PropertySourceBootstrapConfiguration

image-20250721160310842

可以看到这是一个启动配置类,下面的 initialize() 方法就是会调用PropertySourceLocators.locate()来获取远程配置信息,PropertySourceBootstrapConfiguration这个类是 Spring Cloud 中负责处理外部配置源(在这里就是 Nacos 配置中心)的核心启动配置类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 初始化方法,用于处理配置源的加载
* @param applicationContext 可配置的应用上下文
*/
public void initialize(ConfigurableApplicationContext applicationContext) {
// 检查是否需要初始化:如果未启用上下文刷新时初始化,或者环境中不包含"bootstrap"配置源,则执行初始化
if (!this.bootstrapProperties.isInitializeOnContextRefresh() || !applicationContext.getEnvironment().getPropertySources().contains("bootstrap")) {
this.doInitialize(applicationContext);
}

}

/**
* 实际执行初始化的方法,加载并处理配置源
* @param applicationContext 可配置的应用上下文
*/
private void doInitialize(ConfigurableApplicationContext applicationContext) {
// 用于存储复合配置源的列表
List<PropertySource<?>> composite = new ArrayList();
// 对配置源定位器进行排序(按注解@Order指定的顺序)
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
// 标记是否为空配置源
boolean empty = true;
// 获取应用环境
ConfigurableEnvironment environment = applicationContext.getEnvironment();

// 遍历所有配置源定位器
for(PropertySourceLocator locator : this.propertySourceLocators) {
// 通过定位器获取配置源集合
Collection<PropertySource<?>> source = locator.locateCollection(environment);
// 如果配置源不为空
if (source != null && source.size() != 0) {
// 存储处理后的配置源
List<PropertySource<?>> sourceList = new ArrayList();

// 处理每个配置源
for(PropertySource<?> p : source) {
if (p instanceof EnumerablePropertySource) {
// 如果是可枚举的配置源,包装为BootstrapPropertySource
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource)p;
sourceList.add(new BootstrapPropertySource(enumerable));
} else {
// 否则包装为SimpleBootstrapPropertySource
sourceList.add(new SimpleBootstrapPropertySource(p));
}
}

// 日志记录找到的配置源
logger.info("Located property source: " + sourceList);
// 将处理后的配置源添加到复合列表中
composite.addAll(sourceList);
// 标记为非空
empty = false;
}
}
// 此处省略后续对composite列表的处理逻辑
}

到这里,你启动 Nacos ,Nacos 环境准备的最核心配置已经完成了最基本该做的了,接下里就要进行相关配置的加载了,继续从run方法出发,来到上下文准备的位置

image-20250721161601853

this.prepareContext() 方法主要是进行应用上下文的一个准备,这个方法的更多内容可以看我的关于 Spring Boot 主类的解析,说的还是比较清楚的关于刷新和上下文准备)))

image-20250721161756970

准备的这个监听器listeners.contextPrepared(context);,就是监听上下文什么时候完工,然后关闭bootstrapContext

对上下文初始化的方法,就藏在这里this.applyInitializers(context);,其他我还圈上了一些上下文的bean工厂和神秘的懒加载),我们深入这个方法

image-20250721162059408

你会发现我草给我干哪来了,回来了我去,都联系上了,接到了是上面我们说的initialize()方法吗,很可惜不是,但是不是完全不是,而是ApplicationContextInitilaizer的,作用是在应用程序上下文初始化的时候做一些额外的操作,但是最终代码的实现肯定是要跑其子类的代码

image-20250721162418107

而你会发现,PropertySourceBootstrapConfiguration实现了 ApplicationContextInitializerApplicationListener<ContextRefreshedEvent> 接口,所以它肯定能在应用上下文初始化和刷新阶段加入配置。因此上面的方法在执行时,会执行PropertySourceBootstrapConfigurationinitialize()方法,爆回来了

image-20250721162651579

感觉说的有些含糊,为什么 PropertySourceBootstrapConfiguration 这个类实现了ApplicationListener<ContextRefreshedEvent>, ApplicationContextInitializer<ConfigurableApplicationContext>这些内容就能完成加载配置的作用,来看其中的这行代码

1
2
3
4
@Autowired(
required = false
)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList();

PropertySourceLocator接口的主要作用就是实现应用外部化配置可动态加载,而NacosPropertySourceLocator实现了该接口。因此最终会调用NacosPropertySourceLocator中的locate()方法,就像PropertySourceBootstrapConfiguration类中的 initialize() 方法就是会调用PropertySourceLocators.locate()一样,实现把Nacos服务上的代码进行加载。

回去看上面PropertySourceBootstrapConfiguration的代码

1
2
3
4
5
public void initialize(ConfigurableApplicationContext applicationContext) {
if (!this.bootstrapProperties.isInitializeOnContextRefresh() || !applicationContext.getEnvironment().getPropertySources().contains("bootstrap")) {
this.doInitialize(applicationContext);
}
}

这不initialize()方法吗,是的该方法是 ApplicationContextInitializer 接口的实现方法,在应用上下文初始化时被调用。其中的 doInitialize

1
2
3
4
5
6
7
8
9
10
11
// 处理每个配置源
for(PropertySource<?> p : source) {
if (p instanceof EnumerablePropertySource) {
// 如果是可枚举的配置源,包装为BootstrapPropertySource
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource)p;
sourceList.add(new BootstrapPropertySource(enumerable));
} else {
// 否则包装为SimpleBootstrapPropertySource
sourceList.add(new SimpleBootstrapPropertySource(p));
}
}
  • PropertySourceLocator 列表:通过 @Autowired 自动注入所有实现了 PropertySourceLocator 接口的 Bean,NacosPropertySourceLocator 就是其中之一,它负责从 Nacos 服务器获取配置。怎么获取的呢,就是调用每个 locatorlocateCollection 方法,获取从对应配置源(如 Nacos)加载的配置属性源集合。
  • PropertySourceBootstrapProperties:封装了与配置源引导相关的配置属性,例如是否在上下文刷新时初始化等。

然后这些内容会被包装到将处理后的配置源添加到 composite 列表中,并记录日志,composite 列表会被进一步处理,将这些配置源添加到应用上下文的环境中,使得应用能够获取到从外部配置源加载的配置信息,就这块

image-20250721164214836

Nacos 配置中心内部的具体实现细节

终于,我们从 Spring Boot 的自动配置原理,无缝过渡到了 Nacos 配置中心能够对配置进行管理的内容,接下里就是正题了。我们先来到上面提到的 NacosPropertySourceLocator.locate()方法

image-20250721164349654

个方法是 Nacos 配置中心与 Spring 应用集成的关键,负责从 Nacos 服务器加载配置并封装为 Spring 的 PropertySource。最终得到配置中心上的配置并通过对象封装来返回

其中在这里配置了环境变量,这也就是为什么 Nacos 的配置属性能可以直接读取环境变量

1
this.nacosConfigProperties.setEnvironment(env);

在这里获取 Nacos 配置服务

1
2
3
4
5
ConfigService configService = this.nacosConfigManager.getConfigService();
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
}
  • 通过 nacosConfigManager 获取 ConfigService 实例,这是 Nacos 配置客户端的核心 API。

在这里,就有配置源的加载了,而且是依次加载共享配置、扩展配置和应用自身配置顺序进行加载,所以 Nacos 有默认配置,你可以导入配置,各种配置都能自定义配置的基础,最后返回聚合后的复合配置源。

1
2
3
4
5
CompositePropertySource composite = new CompositePropertySource(NACOS_PROPERTY_SOURCE_NAME);
this.loadSharedConfiguration(composite); // 加载 shared-configs 配置的共享配置
this.loadExtConfiguration(composite); // 加载扩展配置
this.loadApplicationConfiguration(composite, dataIdPrefix, this.nacosConfigProperties, env); // 加载应用自身配置
return composite;

到这里,配置的加载流程也就完成了,对于具体加载的内容的机制,因为一般来说我们都是根据应用名称来获取配置,所以这里以loadApplicationConfiguration()方法为例来说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void loadApplicationConfiguration(CompositePropertySource compositePropertySource, String dataIdPrefix,
// 获取配置文件的扩展名(如yaml、properties等)
String fileExtension = properties.getFileExtension();
// 获取Nacos配置的分组
String nacosGroup = properties.getGroup();

// 加载不带扩展名的基础配置(如dataId为"appname"的配置)
// 最后一个参数true表示该配置支持动态刷新
this.loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup, fileExtension, true);

// 加载带标准扩展名的基础配置(如dataId为"appname.yaml"的配置)
this.loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + "." + fileExtension,
nacosGroup,
fileExtension,
true);

// 加载多环境配置(遍历所有激活的环境)
for (String profile : environment.getActiveProfiles()) {
// 构建带环境后缀的dataId(如"appname-dev.yaml")
String dataId = dataIdPrefix + "-" + profile + "." + fileExtension;
// 加载当前环境对应的配置
this.loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup, fileExtension, true);
}
}

这是加载了你的服务项目代码里存在的本身的配置,根据 dataIdPrefixfileExtension 构建 dataId,负责按照 Nacos 的配置命名规则,加载应用的基础配置和多环境配置。也就是,按名加载。

它会先加载基础配置(不带环境后缀),先不带扩展名的配置(如application),然后带标准扩展名的配置(如application.yaml),然后遍历所有激活的环境(profiles),加载对应环境的配置,所有配置通过 loadNacosDataIfPresent 方法实际加载,若 Nacos 服务器存在对应配置,则添加到复合配置源中

不断进入方法,loadApplicationConfiguration()—>loadNacosDataIfPresent()—>NacosPropertySource.loadNacosPropertySource() —>NacosPropertySource.build()—>loadNacosData()

image-20250721165849227

来到

image-20250721165921722

到这一步我们只需了解到,加载的具体操作是交给ConfigService(当然,它是个接口,具体实现交给NacosConfigService来完成)来加载配置的。

Nacos 事件订阅机制在源码中的实现

接下来主要开始说明 NacosConfig 的事件订阅机制的实现在源码中的体现

image-20250721170004112

SpringBoot在启动的时候,会执行准备上下文的这么一个操作。而Nacos有一个类叫做NacosContextRefresher,它实现了ApplicationListener,即他是一个监听器,负责监听准备上下文的事件

image-20250721170134676

onApplicationEventthis.ready.compareAndSet(false, true),这个就是这个事件监听器的相应方法,来看看它触发的方法的内容,这个方法主要用来实现 Nacos 事件监听的注册

1
2
3
4
5
6
7
8
9
10
private void registerNacosListenersForApplications() {
if (this.isRefreshEnabled()) {
for(NacosPropertySource propertySource : NacosPropertySourceRepository.getAll()) {
if (propertySource.isRefreshable()) {
String dataId = propertySource.getDataId();
this.registerNacosListener(propertySource.getGroup(), dataId);
}
}
}
}

其中有着事件刷新的监听器registerNacosListener,这段代码是 Nacos 配置中心实现配置动态刷新的核心逻辑,通过注册监听器实现配置变更时自动通知应用更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 为指定的 Nacos 配置(由 groupKey 和 dataKey 唯一标识)注册监听器。
private void registerNacosListener(final String groupKey, final String dataKey) {
// 监听器key 和实例化监听器
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = (Listener)this.listenerMap.computeIfAbsent(key, (lst) -> new AbstractSharedListener() {
// 实现监听器回调方法
public void innerReceive(String dataId, String group, String configInfo) {
// 配置变更时的回调逻辑
NacosContextRefresher.refreshCountIncrement(); // 增加刷新计数
NacosContextRefresher.this.nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo); // 记录刷新历史
NacosSnapshotConfigManager.putConfigSnapshot(dataId, group, configInfo); // 保存配置快照
NacosContextRefresher.this.applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config")); // 发布刷新事件
if (NacosContextRefresher.log.isDebugEnabled()) {
NacosContextRefresher.log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s", group, dataId, configInfo)); // 记录调试日志
}
}
});

// 使用 configService.addListener 方法将监听器注册到 Nacos 服务器。
try {
this.configService.addListener(dataKey, groupKey, listener);
log.info("[Nacos Config] Listening config: dataId={}, group={}", dataKey, groupKey);
} catch (NacosException e) {
log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey, groupKey), e);
}

}

说多了,赶紧往回调整,来看看NacosConfigService这个类,是 Nacos 配置中心的客户端核心实现类,它是 Nacos 配置中心与应用交互的主要入口,实现了 ConfigService 接口,提供了配置的获取、发布、监听等,真正的核心功能在这

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class NacosConfigService implements ConfigService {
...
private final ClientWorker worker;
private final ConfigFilterChainManager configFilterChainManager;

public NacosConfigService(Properties properties) throws NacosException {
....
// 初始化配置过滤器链管理器
this.configFilterChainManager = new ConfigFilterChainManager(clientProperties.asProperties());
// 初始化服务器列表管理器
ServerListManager serverListManager = new ServerListManager(clientProperties);
serverListManager.start();
// 初始化客户端工作器
this.worker = new ClientWorker(this.configFilterChainManager, serverListManager, clientProperties);
}
}

说一些重要的属性和构造方法里一些比较重要的内容,在考虑这些要单独说吗

  • ClientWorker:客户端工作器,负责与 Nacos 服务器通信,处理配置的拉取和推送。
  • ConfigFilterChainManager:配置过滤器链管理器,用于处理配置的过滤和转换。
  • ServerListManager:服务器列表管理器,负责维护 Nacos 服务器的地址列表。

配置的获取流程就是如下这些方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return this.getConfigInner(this.namespace, dataId, group, timeoutMs);
}

private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
// 优先从本地故障转移配置中获取
String content = LocalConfigInfoProcessor.getFailover(this.worker.getAgentName(), dataId, group, tenant);
if (content != null) {
return content;
}

// 从服务器获取配置
try {
ConfigResponse response = this.worker.getServerConfig(dataId, group, tenant, timeoutMs, false);
return response.getContent();
} catch (NacosException e) {
// 从本地快照中获取配置
content = LocalConfigInfoProcessor.getSnapshot(this.worker.getAgentName(), dataId, group, tenant);
return content;
}
}

而接下来配置了这样的一个监听机制

1
2
3
public void addListener(String dataId, String group, Listener listener) throws NacosException {
this.worker.addTenantListeners(dataId, group, Collections.singletonList(listener));
}
  • ClientWorker 负责维护监听器列表,并通过长轮询机制监听配置变更。
  • 当配置变更时,会触发监听器的回调方法,实现配置的动态更新。

配置发布流程的代码在这

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean publishConfig(String dataId, String group, String content) throws NacosException {
return this.publishConfigInner(this.namespace, dataId, group, null, null, null, content, ConfigType.getDefaultType().getType(), null);
}

private boolean publishConfigInner(String tenant, String dataId, String group, String tag, String appName, String betaIps, String content, String type, String casMd5) throws NacosException {
// 应用配置过滤器
ConfigRequest cr = new ConfigRequest();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
cr.setContent(content);
cr.setType(type);
this.configFilterChainManager.doFilter(cr, null);
content = cr.getContent();

// 发布配置到服务器
return this.worker.publishConfig(dataId, group, tenant, appName, tag, betaIps, content, encryptedDataKey, casMd5, type);
}
  • 配置过滤器:在发布配置前,通过 ConfigFilterChainManager 应用配置过滤器,实现配置的预处理。

还发现了个健康检查机制在这,这都有健康检查的哦真的牛皮

Nacos 配置监听的核心——ClientWorker

来看看 ClientWorker 类是怎么做到维护监听器列表,并通过长轮询机制监听配置变更的,不瞒我说,这个 ClientWorker 类不是 Nacos 中第一难读的,难度肯定也在前三,所以我没法说太细

先来说一下核心职责,首先,它会维护一个维护监听器列表,并通过长轮询机制与服务端通信,实时感知配置变更。

image-20250721171753218

恐怖的代码量和方法调用量,但是很大部分你去看的时候,一眼就行,核心内容还是通过线程池去建立长轮询连接,检查/更新方法的配置的内容来分入手分析。

同样的,我们来看下其构造函数,ClientWorker 的构造函数是整个机制的起点,主要完成线程池初始化核心通信组件创建,为后续定时 Pull 请求奠定基础。

1
2
3
4
5
6
7
8
9
10
11
12
public ClientWorker(ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager, NacosClientProperties properties) throws NacosException {
this.configFilterChainManager = configFilterChainManager;
// 初始化配置
this.init(properties);
// 创建 RPC 传输客户端
this.agent = new ConfigRpcTransportClient(properties, serverListManager);
// 创建定时调度线程池(核心:用于执行定时Pull请求和配置检查)
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(this.initWorkerThreadCount(properties), new NameThreadFactory("com.alibaba.nacos.client.Worker"));
// 将线程池设置到 RPC 客户端
this.agent.setExecutor(executorService);
this.agent.start();
}
  • Executors.newScheduledThreadPool 方法用于创建一个定长的定时调度线程池。线程池是在 ClientWorker 初始化时创建的,用于执行定时任务和配置检查:
  • this.initWorkerThreadCount(properties) 方法确定线程池的线程数量,它会根据配置的参数和默认规则来计算合适的线程数。

线程池ScheduledExecutorService是 Java 并发包中专门用于执行定时任务周期性任务的线程池接口,它的核心作用就是通过预定义的线程资源,按指定时间规则(延迟执行、周期性重复)调度任务,避免频繁创建线程的开销。所以说它能执行包括周期性发送 Pull 请求、检查配置变更等任务。这个线程池的内容就不细讲解了。

长轮询的核心逻辑在 ConfigRpcTransportClient 的定时任务中,它负责实际发送 Pull 请求(长轮询)和处理响应。

我们可以看到在 ClientWorker 初始化时,会创建 ConfigRpcTransportClient 实例并调用其 start() 方法,这里涉及到一个很复杂的调用链,我们梳理一下

1
2
3
4
5
6
public ClientWorker(...) throws NacosException {
// 1. 创建 ConfigRpcTransportClient 实例
this.agent = new ConfigRpcTransportClient(properties, serverListManager);
// 2. 调用 start() 方法启动客户端
this.agent.start();
}

ConfigRpcTransportClient 继承自 ConfigTransportClient,其 start() 方法在父类中定义,核心逻辑是触发初始化:

1
2
3
4
5
6
public void start() throws NacosException {
this.securityProxy.login(this.properties);
this.executor.scheduleWithFixedDelay(() -> this.securityProxy.login(this.properties), 0L, this.securityInfoRefreshIntervalMills, TimeUnit.MILLISECONDS);
// 调用子类的 startInternal() 方法(模板方法模式)
this.startInternal();
}

父类定义 start() 框架,具体初始化逻辑由子类 ConfigRpcTransportClientstartInternal() 实现。而初始化时候, ConfigTransportClientstart() 方法明确调用了 startInternal()

image-20250721195521216

我们的 ClientWorker 的属性引入了ConfigRpcTransportClient方法,private final ConfigRpcTransportClient agent;,然后把ConfigRpcTransportClient方法写成了ClientWorker 的内部类,继承了ConfigTransportClient

image-20250721195730739

而可以看到在 ConfigRpcTransportClient 类的 startInternal 方法中,启动了一个定时任务:

这就实现了整条完整的调用链,ClientWorker 构造函数new ConfigRpcTransportClient()(创建实例) → this.agent.start()(调用父类方法) → 父类 start() → 子类 startInternal()(执行定时任务逻辑)。

ConfigRpcTransportClientstartInternal() 方法是定时 Pull 请求的入口,其核心是启动一个阻塞队列 + 循环任务,实现 “按需触发 + 定时检查” 的 Pull 机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void startInternal() {
// this.executor.schedule 方法向线程池提交一个延迟执行的任务,这里延迟时间为 0 毫秒,即立即执行。
this.executor.schedule(() -> {
while(!this.executor.isShutdown() && !this.executor.isTerminated()) {
try {
// 然后尝试从 listenExecutebell 这个阻塞队列中获取元素,等待超时时间为 5 秒
this.listenExecutebell.poll(5L, TimeUnit.SECONDS);
if (!this.executor.isShutdown() && !this.executor.isTerminated()) {
// 执行配置检查(核心:发送Pull请求)
this.executeConfigListen();
}
} catch (Throwable e) {
ClientWorker.LOGGER.error("[rpc listen execute] [rpc listen] exception", e);
try {
// 如果在执行过程中出现异常,会记录错误日志,并通过 this.notifyListenConfig() 方法通知重新监听配置,同时让线程休眠 50 毫秒。
Thread.sleep(50L);
} catch (InterruptedException var3) {
}
this.notifyListenConfig();
}
}
}, 0L, TimeUnit.MILLISECONDS);
}

startInternal方法中,我们可以看到,调用了this.executeConfigListen();,这是配置检查与更新的核心方法,是长轮询的核心实现,主要职责是负责检查配置是否变更并更新本地缓存,当配置发生变更肯定要通知到各个服务端

image-20250721193456246

其工作流程是遍历本地缓存的配置,筛选需要监听的配置项,并通过 checkListenCache() 向服务端发送 Pull 请求(长轮询),检查是否有变更。配置变更通知类(executeConfigListen)如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public void executeConfigListen() throws NacosException {
// 分组管理需要监听的配置和需要移除的配置
Map<String, List<CacheData>> listenCachesMap = new HashMap(16);
Map<String, List<CacheData>> removeListenCachesMap = new HashMap(16);

// 遍历所有本地缓存的配置(CacheData)
for (CacheData cache : ((Map) ClientWorker.this.cacheMap.get()).values()) {
synchronized (cache) {
// 检查本地配置(故障转移文件等)
this.checkLocalConfig(cache);

// 如果配置与服务器一致且无需全量同步,则跳过
if (cache.isConsistentWithServer()) {
cache.checkListenerMd5(); // 检查监听器MD5,确保配置变更已通知
if (!needAllSync) continue;
}

// 区分需要监听和需要移除的配置
if (!cache.isUseLocalConfigInfo()) { // 不使用本地配置时
if (!cache.isDiscard()) { // 未被丢弃的配置,加入监听列表
List<CacheData> cacheDatas = listenCachesMap.computeIfAbsent(
String.valueOf(cache.getTaskId()),
k -> new LinkedList<>()
);
cacheDatas.add(cache);
} else { // 已被丢弃的配置,加入移除列表
List<CacheData> cacheDatas = removeListenCachesMap.computeIfAbsent(
String.valueOf(cache.getTaskId()),
k -> new LinkedList<>()
);
cacheDatas.add(cache);
}
}
}
}

// 检查并更新监听的配置(核心:发送长轮询Pull请求)
boolean hasChangedKeys = this.checkListenCache(listenCachesMap);
// 移除不需要监听的配置
this.checkRemoveListenCache(removeListenCachesMap);

// 若有变更,立即触发下一次检查(避免遗漏)
if (hasChangedKeys) {
this.notifyListenConfig();
}
}
  • 本地缓存(cacheMap)ClientWorker 维护一个 cacheMap,存储客户端已订阅的配置(CacheData 实例),每个 CacheData 包含配置的 dataId、group、MD5、监听器列表等信息。

  • 筛选逻辑:仅对 “未被丢弃” 且 “与服务端可能不一致” 的配置发送 Pull 请求,减少无效通信。

接着在我们的executeConfigListen()配置检查与更新方法中,我们可以看到其中的checkListenCache()方法 image-20250721193906814

这个方法是通过线程池提交任务,向 Nacos 服务器发送长轮询请求,检查配置是否变更,可以说是核心中的核心,代码太长了沃日,我只总结部分重要的内容,剩下的省略了

这个方法是定时 Pull 请求的实际执行者,用于检查监听的配置缓存是否有变更,向 Nacos 服务器发送批量监听请求,然后接收并处理服务器返回的变更配置列表,收到更改,刷新配置项完成操作(异步)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private boolean checkListenCache(Map<String, List<CacheData>> listenCachesMap) throws NacosException {
AtomicBoolean hasChangedKeys = new AtomicBoolean(false);
if (!listenCachesMap.isEmpty()) {
List<Future> listenFutures = new ArrayList<>();

// 遍历监听列表,按任务ID分组处理
for (Map.Entry<String, List<CacheData>> entry : listenCachesMap.entrySet()) {
String taskId = entry.getKey();
RpcClient rpcClient = this.ensureRpcClient(taskId); // 获取RPC客户端
ExecutorService executorService = this.ensureSyncExecutor(taskId); // 获取执行线程池

// 提交异步任务:发送Pull请求
Future future = executorService.submit(() -> {
List<CacheData> listenCaches = entry.getValue();
// 重置变更标识
for (CacheData cacheData : listenCaches) {
cacheData.getReceiveNotifyChanged().set(false);
}

// 构建批量监听请求(包含当前客户端的配置MD5)
ConfigBatchListenRequest request = this.buildConfigRequest(listenCaches);
request.setListen(true); // 标记为长轮询

try {
// 发送请求并等待响应(长轮询:服务端若有变更则立即返回,否则等待30秒超时)
ConfigChangeBatchListenResponse response =
(ConfigChangeBatchListenResponse) this.requestProxy(rpcClient, request);

if (response != null && response.isSuccess()) {
List<ConfigChangeBatchListenResponse.ConfigContext> changedConfigs = response.getChangedConfigs();

// 处理服务端返回的变更配置
if (!CollectionUtils.isEmpty(changedConfigs)) {
hasChangedKeys.set(true);
for (ConfigChangeBatchListenResponse.ConfigContext changeConfig : changedConfigs) {
String changeKey = GroupKey.getKeyTenant(changeConfig.getDataId(), changeConfig.getGroup(), changeConfig.getTenant());
// 刷新本地配置并通知监听器
this.refreshContentAndCheck(rpcClient, changeKey, changeConfig);
}
........
}
return hasChangedKeys.get();
}
  • 长轮询机制:客户端发送的 Pull 请求会在服务端 “挂起” 最多 30 秒(默认超时时间)。若期间配置发生变更,服务端会立即返回变更结果;若超时未变更,服务端返回空响应,客户端则立即发送下一次 Pull 请求,形成 “持续监听”。

  • 批量处理:客户端将多个配置项打包成批量请求发送,减少网络交互次数,提升效率。

  • 变更处理:若服务端返回变更配置,客户端通过 refreshContentAndCheck() 拉取最新配置内容,更新本地 CacheData 的 MD5 和值,并触发监听器通知应用。

最后,我们来了解这个类中其他一堆另外作用的方法,忽略过一大堆缓存相关的配置和存储读取检查的内容,不多说,直接来到 配置监听机制 (addListenersaddTenantListeners)

ClientWorker 不仅负责发送 Pull 请求,还维护着配置监听器列表,当配置变更时,通过监听器触发应用层面的更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void addListeners(String dataId, String group, List<? extends Listener> listeners) throws NacosException {
group = this.blank2defaultGroup(group);
// 检查并创建本地缓存(CacheData)
CacheData cache = this.addCacheDataIfAbsent(dataId, group);
synchronized (cache) {
// 将监听器添加到CacheData中
for (Listener listener : listeners) {
cache.addListener(listener);
}
// 标记配置不一致,触发立即检查
cache.setDiscard(false);
cache.setConsistentWithServer(false);
this.agent.notifyListenConfig(); // 向阻塞队列添加元素,触发executeConfigListen()
}
}
  • 当应用添加监听器时,ClientWorker 会为对应的配置项创建 CacheData 并关联监听器,同时通过 notifyListenConfig() 立即触发一次 Pull 请求,确保客户端与服务端配置同步。

checkListenCache() 检测到配置变更并更新 CacheData 后,CacheData 会通过 checkListenerMd5() 对比配置的 MD5 变化,若不一致则触发相关联监听器的 safeNotifyListener 方法,通知应用配置已更新。

1
2
3
4
5
6
7
void checkListenerMd5() {
for(ManagerListenerWrap wrap : this.listeners) {
if (!this.md5.equals(wrap.lastCallMd5)) {
this.safeNotifyListener(this.dataId, this.group, this.content, this.type, this.md5, this.encryptedDataKey, wrap);
}
}
}

然后紧跟着的是配置获取流程

1
2
3
4
5
6
7
8
9
public ConfigResponse getServerConfig(String dataId, String group, String tenant, long readTimeout, boolean notify) throws NacosException {
// 从默认分组获取配置
if (StringUtils.isBlank(group)) {
group = "DEFAULT_GROUP";
}

// 调用 RPC 客户端的 queryConfig 方法。
return this.agent.queryConfig(dataId, group, tenant, readTimeout, notify);
}

配置发布就是调用了调用 RPC 客户端的 publishConfig 方法。然后将发生变更了的配置发布到 Nacos 服务器。

1
2
3
public boolean publishConfig(String dataId, String group, String tenant, String appName, String tag, String betaIps, String content, String encryptedDataKey, String casMd5, String type) throws NacosException {
return this.agent.publishConfig(dataId, group, tenant, appName, tag, betaIps, content, encryptedDataKey, casMd5, type);
}

到这里我们主要讲了,Nacos 如何实现 Nacos 事件监听的注册 ,如何实现配置动态刷新的机制,上下文监听后配置如何进行刷新和通知,接下来,我们就需要讲 Nacos Client 是如何定时发送 pull 请求然后得到动态刷新模块发送的发生更改的配置。

总结一下,Nacos Client 通过以下流程实现配置刷新:

  1. 初始化ClientWorker 创建定时线程池,初始化 ConfigRpcTransportClient 作为通信代理。
  2. 定时任务启动ConfigRpcTransportClientstartInternal() 启动循环任务,每 5 秒(或按需)触发 executeConfigListen()
  3. 筛选配置executeConfigListen() 遍历本地缓存,筛选需要监听的配置项。
  4. 发送 Pull 请求checkListenCache() 通过线程池向服务端发送长轮询请求,批量检查配置变更。
  5. 处理变更:若服务端返回变更,更新本地 CacheData,并通过监听器通知应用。
  6. 循环监听:无论是否有变更,客户端都会持续发送 Pull 请求,确保实时感知配置变化。

Nacos Client 是如何定时发送 pull 请求实现配置刷新

来找到 Nacos Client 包,其中定时发送 Pull 请求实现配置刷新涉及多个包和类,主要逻辑集中在客户端与服务端交互以及配置监听相关的代码,就比如,上面我们讲的ClientWorker类就在com.alibaba.nacos.client.config.impl.ClientWorker,如果你觉得我少东西了,你可以去摸摸这种包

在上述 ClientWorker 类中的构造方法中,我们讲解了主要内容是创建定时调度线程池,创建好线程池后,通过 this.agent.setExecutor(executorService) 将线程池设置到 ConfigRpcTransportClient 中。我们进去这个方法

服务器处理长连接机制

引用自https://blog.csdn.net/Zong_0915/article/details/113089265,并且本篇文章提供了大量帮助,感谢

前面主要是讲了事件的订阅、WorkClient创建出的线程池干了什么、以及长连接的建立,但是这些都是面向客户端的,因此接下来从服务端的角度来看一看长连接的处理机制。

在Nacos-config模块下,controller包下有一个类叫做ConfigController,专门用来实现配置的基本操作,其中有一个/listener接口,是客户端发起数据监听的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@PostMapping("/listener")
@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
public void listener(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
String probeModify = request.getParameter("Listening-Configs");
if (StringUtils.isBlank(probeModify)) {
throw new IllegalArgumentException("invalid probeModify");
}

probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);

Map<String, String> clientMd5Map;
try {
//获取客户端需要监听的可能发生变化的配置,并计算其MD5值。
clientMd5Map = MD5Util.getClientMd5Map(probeModify);
} catch (Throwable e) {
throw new IllegalArgumentException("invalid probeModify");
}

// 执行长轮询的请求。
inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}

主要干两件事:

  1. 获取客户端需要监听的可能发生变化的配置,并计算其MD5值。
  2. inner.doPollingConfig()开始执行长轮询的请求。

接下来来看一下处理长轮询的方法doPollingConfig()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
Map<String, String> clientMd5Map, int probeRequestSize) throws IOException {

// 首先判断当前请求是否为长轮询,如果是,则调用addLongPollingClient()方法
if (LongPollingService.isSupportLongPolling(request)) {
longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
return HttpServletResponse.SC_OK + "";
}

// 兼容短轮询的逻辑
List<String> changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);

// 兼容短轮询的结果
String oldResult = MD5Util.compareMd5OldResult(changedGroups);
String newResult = MD5Util.compareMd5ResultString(changedGroups);
// 省略。。
}

//addLongPollingClient()方法
public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
int probeRequestSize) {
//获取客户端设置的请求超时时间
String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
String tag = req.getHeader("Vipserver-Tag");
int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);

// Add delay time for LoadBalance, and one response is returned 500 ms in advance to avoid client timeout.
// 意思是提前500ms返回响应,为了避免客户端超时
long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
// 判断是否为混合连接,如果是,那么定时任务将在30秒后开始执行
if (isFixedPolling()) {
timeout = Math.max(10000, getFixedPollingInterval());
// Do nothing but set fix polling timeout.
} else {
// 如果不是,29.5秒后开始执行,也就是所谓的等待期
long start = System.currentTimeMillis();
// 和服务端的数据进行MD5对比,如果发生变化则直接返回
List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
if (changedGroups.size() > 0) {
generateResponse(req, rsp, changedGroups);
LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "instant",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
} else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
LogUtil.CLIENT_LOG.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
changedGroups.size());
return;
}
}
String ip = RequestUtil.getRemoteIp(req);

// Must be called by http thread, or send response.
// 一定要由HTTP线程调用,否则离开后容器会立即发送响应
final AsyncContext asyncContext = req.startAsync();

// AsyncContext.setTimeout() is incorrect, Control by oneself
asyncContext.setTimeout(0L);
// 执行ClientLongPolling线程
ConfigExecutor.executeLongPolling(
new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
}

即将客户端的长轮询请求封装成ClientLongPolling交给scheduler执行。

ClientLongPolling同样是一个线程,因此也看他的run()方法,主要做四件事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Override
public void run() {
// 1.创建一个调度的任务,调度的延时时间为 29.5s
asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(new Runnable() {
@Override
public void run() {
try {
getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());

// 3.延时时间到了之后,首先将该 ClientLongPolling 自身的实例从 allSubs 中移除
allSubs.remove(ClientLongPolling.this);

if (isFixedPolling()) {
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "fix",
RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
"polling", clientMd5Map.size(), probeRequestSize);
// 4.获取服务端中保存的对应客户端请求的groupKeys,判断是否变更,并将结果写入response,返回给客户端
List<String> changedGroups = MD5Util
.compareMd5((HttpServletRequest) asyncContext.getRequest(),
(HttpServletResponse) asyncContext.getResponse(), clientMd5Map);
if (changedGroups.size() > 0) {
sendResponse(changedGroups);
} else {
sendResponse(null);
}
} else {
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - createTime), "timeout",
RequestUtil.getRemoteIp((HttpServletRequest) asyncContext.getRequest()),
"polling", clientMd5Map.size(), probeRequestSize);
sendResponse(null);
}
} catch (Throwable t) {
LogUtil.DEFAULT_LOG.error("long polling error:" + t.getMessage(), t.getCause());
}

}

}, timeoutTime, TimeUnit.MILLISECONDS);
// 2.将ClientLongPolling自身的实例添加到一个allSubs中
// 他是一个队列ConcurrentLinkedQueue<ClientLongPolling>
allSubs.add(this);
}

这里可以这么理解:

  • allSubs这个队列和ClientLongPolling之间维持了一种订阅关系,而ClientLongPolling是属于被订阅的角色。
  • 那么一旦订阅关系删除后,订阅方就无法对被订阅方进行通知了。
  • 服务端直到调度任务的延时时间用完之前,ClientLongPolling都不会有其他的事情可以做,因此这段时间内allSubs队列会处理相关的逻辑。

为了我们在客户端长轮询期间,一旦更改配置,客户端能够立即得到响应数据,因此这个事件的触发肯定需要发生在服务端上。看下ConfigController下的publishConfig()方法

我在这里直接以截图的形式来展示重要的代码逻辑:

image-20250721200434772

到这里,如果我们从Nacos控制台上更新了某个配置项后,这里会调用LongPollingServiceonEvent()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 public LongPollingService() {
allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();

ConfigExecutor.scheduleLongPolling(new StatTask(), 0L, 10L, TimeUnit.SECONDS);

// Register LocalDataChangeEvent to NotifyCenter.
NotifyCenter.registerToPublisher(LocalDataChangeEvent.class, NotifyCenter.ringBufferSize);

// Register A Subscriber to subscribe LocalDataChangeEvent.
NotifyCenter.registerSubscriber(new Subscriber() {

@Override
public void onEvent(Event event) {
if (isFixedPolling()) {
// Ignore.
} else {
if (event instanceof LocalDataChangeEvent) {
LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
}
}
}

@Override
public Class<? extends Event> subscribeType() {
return LocalDataChangeEvent.class;
}
});
}

意思就是通过DataChangeTask这个任务来通知客户端:”服务端的数据已经发生了变更!“,接下来看下这个任务干了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class DataChangeTask implements Runnable { 
@Override
public void run() {
try {
ConfigCacheService.getContentBetaMd5(groupKey);
// 1.遍历allSubs队列,该队列中维持的是所有客户端的请求任务
// 那么需要找到与当前发生变更的配置项的groupKey相等的ClientLongPolling任务
for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
ClientLongPolling clientSub = iter.next();
if (clientSub.clientMd5Map.containsKey(groupKey)) {
// If published tag is not in the beta list, then it skipped.
if (isBeta && !CollectionUtils.contains(betaIps, clientSub.ip)) {
continue;
}

// If published tag is not in the tag list, then it skipped.
if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
continue;
}

getRetainIps().put(clientSub.ip, System.currentTimeMillis());
iter.remove(); // 删除订阅关系
LogUtil.CLIENT_LOG
.info("{}|{}|{}|{}|{}|{}|{}", (System.currentTimeMillis() - changeTime), "in-advance",
RequestUtil
.getRemoteIp((HttpServletRequest) clientSub.asyncContext.getRequest()),
"polling", clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
// 2.找到对应的ClientLongPolling任务后,将发生变更的groupKey通过该ClientLongPolling写入到响应对象中,即完成一次数据变更的推送操作。
clientSub.sendResponse(Arrays.asList(groupKey));
}
}
} catch (Throwable t) {
LogUtil.DEFAULT_LOG.error("data change error: {}", ExceptionUtil.getStackTrace(t));
}
}
}


void sendResponse(List<String> changedGroups) {

// Cancel time out task.
// 如果说DataChangeTask完成了数据的推送,ClientLongPolling中的调度任务又开始执行了,那么会发生冲突
// 因此在进行推送操作之前,现将原来等待执行的调度任务取消掉
// 这样就可以防止推送操作完成后,调度任务又去写响应数据,造成冲突。
if (null != asyncTimeoutFuture) {
asyncTimeoutFuture.cancel(false);
}
generateResponse(changedGroups);
}

总结:

  • 为什么更改了配置信息后客户端会立即得到响应?
    1. 首先每个配置在服务端都封装成一个ClientLongPolling对象。其存储于队列当中。
    2. 客户端和服务端会建立起一个长连接,并且维持29.5秒的等待时间,这段时间内除非配置发生更改,请求是不会返回的。
    3. 其次服务端一旦发现配置信息发生更改,在更改了配置信息后,会找到对应的ClientLongPolling任务,并将其更改后的groupKey写入到响应对象中response,进行立刻返回。
    4. 之所以称之为实时的感知,是因为服务端主动将变更后的数据通过HTTP的response对象写入并且立刻返回。
    5. 而服务端说白了,就是做了一个定时调度任务,在等待调度任务执行的期间(29.5秒)若发生配置变化,则立刻响应,否则等待30秒去返回配置数据给客户端。

Nacos 源码阅读的总结

总结一下:

Nacos Config 的配置加载过程如下:

  • Spring Boot项目启动,执行SpringApplication.run()方法,先对项目所需的环境做出准备
  • BootstrapApplicationListener监听器监听到环境准备事件,对需要做自动装配的类进行载入。,导入BootstrapImportSelectorConfiguration配置类,该配置类引入BootstrapImportSelector选择器,完成相关的初始化操作。
  • 环境准备完成后(所需的相关配置类也初始化完成),执行方法this.prepareContext()完成上下文信息的准备。
  • his.prepareContext()需要对相关的类进行初始化操作。由于PropertySourceBootstrapConfiguration类实现了ApplicationContextInitializer接口。因此调用其initialize()方法,完成初始化操作。
  • 对于PropertySourceBootstrapConfiguration下的初始化操作,需要实现应用外部化配置可动态加载,而NacosPropertySourceLocator 实现了PropertySourceLocator接口,故执行他的locate()方法。
  • 最终NacosPropertySourceLocatorlocate()方法完成从Nacos Config Server上加载配置信息。

接下来开始说Nacos Config实时更新的一个原理:

首先,对于客户端而言,如何感知到服务端配置的变更呢?

  • 同样的,当SpringBoot项目启动的时候,会执行”准备上下文“的这么一个事情。
  • 此时NacosContextRefresher会监听到这个事件,并且注册一个负责监听配置变更回调的监听器registerNacosListener。通过注册监听器实现配置变更的时候自动通知Nacos
  • registerNacosListener一旦收到配置变更的回调,则由NacosContextRefresher调用publishEvent发布刷新事件
  • 通过 ConfigService.addListenerClientWorker 注册监听器,注册完成后,ClientWorker 会通过 notifyListenConfig() 向阻塞队列发送信号,触发一次即时的配置检查(避免错过注册前已发生的变更)。
  • 一旦发现服务端配置的变更,那么客户端肯定是要再进行配置的加载(locate())的而其最终通过NacosConfigService.getConfig()方法来实现,在调用这个方法之前,必定要完成NacosConfigService的初始化操作。
    • 根据其中根据NacosConfigService的构造函数,其中初始化一个ClientWorker。初始化一个配置的过滤器链
    • 初始化ClientWorker的过程中,构建了定时调度的线程池executorService,来进行定时的Pull请求和配置检查的内容
    • ClientWorker 通过 ConfigRpcTransportClient 维护的定时任务,以长轮询方式持续与服务端通信,感知配置变更
    • startInternal() 方法启动的循环任务中,每 5 秒(或按需触发)执行 executeConfigListen(),通过 checkListenCache() 向服务端发送批量 Pull 请求。
    • 服务端返回的变更列表中,ClientWorker 会对比本地 CacheData 的 MD5 与服务端新 MD5,确认配置确实变更。
    • checkListenCache() 检测到配置变更后,客户端会通过 refreshContentAndCheck() 调用 ConfigService.getConfig(),从服务端获取变更配置的完整内容。
    • 本地缓存更新后,ClientWorker 会通过 CacheData.checkListenerMd5() 触发监听器回调,完成应用配置的动态刷新
    • 调用监听器的 safeNotifyListener 方法,将新配置内容传递给应用。对于 Spring 应用,这里的监听器由 NacosContextRefresher 注册
    • NacosContextRefresher 调用 publishEvent 发布 RefreshEvent 事件。
    • Spring Cloud 的 RefreshEventListener 监听该事件,触发环境变量刷新(ContextRefresher.refresh())。
    • 刷新过程中,Spring 会重新绑定 @ConfigurationProperties@Value 注解的 Bean,使新配置生效。

Nacos 配置中心与 Spring 应用的集成核心在于配置加载与聚合,其核心逻辑集中在 NacosPropertySourceLocator.locate() 方法中。

  • 首先,NacosPropertySourceLocator.locate() 方法负责从 Nacos 服务器获取配置并封装为 Spring 可识别的 PropertySource
  • 通过 nacosConfigManager 获取 ConfigService(实际实现为 NacosConfigService),作为与 Nacos 服务器交互的核心 API。获取到服务器实例
  • 然后创建 CompositePropertySource 用于聚合多来源配置,按顺序加载三类配置(共享配置、扩展配置、应用自身配置),最终返回聚合结果。
  • 之后loadNacosDataIfPresent() 是具体加载单个配置的方法,其核心逻辑依赖 ConfigService 完成,最终通过 ConfigService.getConfig() 从 Nacos 服务器获取配置内容(支持缓存、动态刷新等机制),并封装为 NacosPropertySource 后添加到聚合结果中。

这也就是我目前为止写过最长的一篇文章了

本来我还想写一下 Nacos 的在配置中心上的实际应用,但是好像没啥好写的,这玩意一上手感觉就会用其实

强迫症犯了,不写感觉不完整

Nacos 配置中心的实践

(二编:这里基本废弃,我打算单开一个文章来讲解,因为一梳理,发现东西确实不少,有必要拿出来)

添加如下配置文件,服务消费者也一样

1
2
3
4
5
6
7
8
9
10
11
12
spring:
application:
name: nacos-provider
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
# 指定命名空间,默认为public
namespace: public
# 指定配置组,默认为DEFAULT_GROUP
group: DEFAULT_GROUP

简要介绍一下 bootstrap 配置文件,,bootstrap 配置文件是一种特殊的配置文件,其核心作用是在应用上下文初始化的早期阶段加载关键配置,为应用的启动和核心组件(如服务注册发现、配置中心等)的初始化提供必要参数。

一般知道,bootstrap 配置文件(bootstrap.propertiesbootstrap.yaml)的加载优先级高于普通的 application 配置文件,会在 Spring 应用上下文初始化的最早期被加载,所以一般微服务的时候用的更多

启动 Nacos,像这样创建配置,Data ID一般都是服务名+文件扩展名

image-20250721232622067

写一个配置的控制器类,方便能查看效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/config")
@RefreshScope // 支持配置动态刷新
public class ConfigController {

@Value("${provider.config.name:default-name}")
private String name;

@Value("${provider.config.version:1.0}")
private String version;

@Value("${provider.config.env:local}")
private String env;

@GetMapping("/info")
public Map<String, String> getConfigInfo() {
Map<String, String> configInfo = new HashMap<>();
configInfo.put("name", name);
configInfo.put("version", version);
configInfo.put("env", env);
return configInfo;
}
}

然后试着访问上面我们配置的接口,可以看到是没问题的,我们接下来进行修改

image-20250721232843800

你直接在spring里面修改和在 Nacos 里面修改都可以,为了体现其配置中心性质))在 Nacos 改

image-20250721233012474

修改之后,再次访问,发现配置更改

多环境切换和共享配置先不演示了

共享配置就是

  • 在Nacos中创建共享配置:

    • Data ID: common-config.yaml

    • Group: DEFAULT_GROUP

  • 然后在 bootstrap.yml 中把共享配置加进去

    1
    2
    3
    4
    5
    6
    7
    8
    spring:
    cloud:
    nacos:
    config:
    shared-configs:
    - data-id: common-config.yaml
    group: DEFAULT_GROUP
    refresh: true

Nacos 的环境隔离

Nacos 中服务存储和数据存储的最外层都是 namespace 的位置,用来做最外层的隔离,Nacos 默认的命名空间是 public

你也可以基于 Group 进行隔离,这就涉及到集群的设置了,一般是一个地域的集群设置成一个分组 Group 隔离

或者把一些业务相关度比较高的服务放在一个 Group 进行数据隔离

设置命名空间

image-20250722163057528

按照如下形式修改你的服务的命名空间,注意 填写的是命名空间的 ID

image-20250722163158049