在系统开发过程中通常会将一些需要变更的参数、变量等从代码中分离出来独立管理,以独立的配置文件的形式存在。目的是让静态的系统工件或者交付物(如 WAR,JAR 包等)更好地和实际的物理运行环境进行适配。配置管理一般包含在系统部署的过程中,由系统管理员或者运维人员完成这个步骤。配置变更是调整系统运行时的行为的有效手段之一。

命名空间(Namespace)

用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。Namespace 的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如数据库配置、限流阈值、降级开关)隔离等。如果在没有指定 Namespace 的情况下,默认使用 public 命名空间。

配置组(Group)

Nacos 中的一组配置集,是配置的维度之一。通过一个有意义的字符串对配置集进行分组,从而区分 Data ID 相同的配置集。当您在 Nacos 上创建一个配置时,如果未填写配置分组的名称,则配置分组的名称默认采用 DEFAULT_GROUP 。配置分组的常见场景:不同的应用或组件使用了相同的配置项,如 database_url 配置和 MQ_Topic 配置。

配置ID(Data ID)

Nacos 中的某个配置集的 ID。配置集 ID 是划分配置的维度之一。Data ID 通常用于划分系统的配置集。一个系统或者应用可以包含多个配置集,每个配置集都可以被一个有意义的名称标识。Data ID 尽量保障全局唯一,这里一般在nacos配置中心配置就是指的文件名。

配置中心依赖包spring-cloud-starter-alibaba-nacos-config,通过SPI机制来完成配置的初始化。

启动初始化加载

其中自动装配配置类NacosConfigBootstrapConfiguration会在springboot启动时进行依次配置文件的加载。该配置类负责把 NacosConfigManagerNacosPropertySourceLocatorNacosConfigProperties 等 bean 注入到 Spring 容器,NacosConfigManager里根据nacos config的配置信息,创建维护了ConfigService,和配置中心的通讯通过ConfigService。

Spring Cloud Config 的核心机制是实现 PropertySourceLocator 接口,Nacos 也不例外。NacosPropertySourceLocator就实现了PropertySourceLocator 接口。springboot Environment初始化时会调用其locate()方法从nacos配置中心拉取配置文件内容,拉到的配置会封装成 PropertySource,加入到 Spring 的 Environment 里。所以应用一启动,配置就能直接注入到 @Value@ConfigurationProperties 里。

NacosPropertySourceLocator#locate()

	public PropertySource<?> locate(Environment env) {
		nacosConfigProperties.setEnvironment(env);
        //获取ConfigService
		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();
        //configService放入nacosPropertySourceBuilder变量中,后面的获取文件都是通过这个Builder
		nacosPropertySourceBuilder变量中,后面的 = new NacosPropertySourceBuilder(configService,
				timeout);
		String name = nacosConfigProperties.getName();
		//获取spring.cloud.nacos.config.prefix指定的固定文件前缀
		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);
		//加载spring.cloud.nacos.config.shared-configs 指定的配置文件
		loadSharedConfiguration(composite);
        //加载spring.cloud.nacos.config.extension-configs指定的配置文件
		loadExtConfiguration(composite);
        //加载和application名称,profile相关的默认配置文件
		loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
		return composite;
	}

获取配置文件都是通过loadNacosPropertySource()方法

	private NacosPropertySource loadNacosPropertySource(final String dataId,
			final String group, String fileExtension, boolean isRefreshable) {
		if (NacosContextRefresher.getRefreshCount() != 0) {
			if (!isRefreshable) {
				return NacosPropertySourceRepository.getNacosPropertySource(dataId,
						group);
			}
		}
		/**
		  从配置中心拉取配置
		  dataId:资源Id,一般是文件名
		  group:所属分组
		  fileExtension:文件扩展名
		*/
		return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
				isRefreshable);
	}

上面说了nacosPropertySourceBuilder里面保存有configService,最后真正拉取配置的方法是loadNacosData()

NacosPropertySourceBuilder#loadNacosData()

	private List<PropertySource<?>> loadNacosData(String dataId, String group,
			String fileExtension) {
		String data = null;
		try {
		    //从nacos中读取配置
			data = configService.getConfig(dataId, group, timeout);
			if (StringUtils.isEmpty(data)) {
				log.warn(
						"Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
						dataId, group);
				return Collections.emptyList();
			}
			if (log.isDebugEnabled()) {
				log.debug(String.format(
						"Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
						group, data));
			}
			//转换成PropertySource
			return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,
					fileExtension);
		}
		catch (NacosException e) {
			log.error("get data from Nacos error,dataId:{} ", dataId, e);
		}
		catch (Exception e) {
			log.error("parse data from Nacos error,dataId:{},data:{}", dataId, data, e);
		}
		return Collections.emptyList();
	}

这里看到是通过调用 NacosConfigService.getConfig 去拉取 Nacos 里的配置。返回是一个字符串类型,然后使用NacosDataParserHandler转换成PropertySource。

自动刷新

应用可以通过配置spring.cloud.nacos.config.refresh-enable:true属性来控制应用配置自动刷新,也就是配置信息在配置中心修改后,应用程序可以自动刷新拉取最新的配置,在nacos config starter中另一个自动装配类NacosConfigAutoConfiguration中,会初始化一个NacosContextRefresher自动注册配置监听器。NacosContextRefresher实现了ApplicationListener<ApplicationReadyEvent> 接口,当容器初始化完成后会调用其onApplicationEvent(),通过其registerNacosListenersForApplications()方法来注册配置监听listener。

NacosContextRefresher#registerNacosListenersForApplications()

	private void registerNacosListenersForApplications() {
		if (isRefreshEnabled()) {//配置为自动刷新
			for (NacosPropertySource propertySource : NacosPropertySourceRepository
					.getAll()) {
				if (!propertySource.isRefreshable()) {
					continue;
				}
				String dataId = propertySource.getDataId();
                //加载过的配置(dataId, group)订阅事件
				registerNacosListener(propertySource.getGroup(), dataId);
			}
		}
	}

事件订阅在registerNacosListener(),底层还是走的 NacosConfigService.addListenerClientWorkerListener.receiveConfigInfo

	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);
                        //发布RefreshEvent事件
						applicationContext.publishEvent(
								new RefreshEvent事件(this, null, "Refresh Nacos config"));
						if (log.isDebugEnabled()) {
							log.debug(String.format(
									"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
									group, dataId, configInfo));
						}
					}
				});
		try {
          //添加Listener
			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);
		}
	}

当 Nacos 配置发生变化时,它会触发 Spring Cloud 的 RefreshEvent。配合 @RefreshScope,Bean 会被销毁重建,从而达到动态刷新效果。

本地快照

nacos每次获取到配置后,ClientWorker 会调用工具类LocalConfigInfoProcessor把数据写入 ${user.home}/nacos/config/ 下的文件。获取配置时,先尝试远程,如果失败就读这个快照。本地快照类似于缓存,保证即使 Nacos Server 挂了,应用也能继续运行。

ClientWorker

ClientWorker是在NacosConfigService中的一个内部属性。ClientWorker 是 Nacos 客户端架构中一个至关重要的后台工作线程。所有客户端和服务端数据同步都是通过ClientWorker来完成。

其构造方法会初始化一个ConfigRpcTransportClient实例作为数据同步的rpc请求客户端,另外初始化一个ScheduledExecutor用来定期执行数据同步任务。

    public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager,
            final NacosClientProperties properties) throws NacosException {
        this.configFilterChainManager = configFilterChainManager;
        
        init(properties);
        
        agent = new ConfigRpcTransportClient(properties, serverListManager);
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(initWorkerThreadCount(properties),
                new NameThreadFactory("com.alibaba.nacos.client.Worker"));
        agent.setExecutor(executorService);
        agent.start();
        
    }

ConfigRpcTransportClient在start()方法通过startInternal()来完成线程任务启动

        public void startInternal() {
            executor.schedule(() -> {
                while (!executor.isShutdown() && !executor.isTerminated()) {
                    try {
                        listenExecutebell.poll(5L, TimeUnit.SECONDS);
                        if (executor.isShutdown() || executor.isTerminated()) {
                            continue;
                        }
                        executeConfigListen();
                    } catch (Throwable e) {
                        LOGGER.error("[rpc listen execute] [rpc listen] exception", e);
                        try {
                            Thread.sleep(50L);
                        } catch (InterruptedException interruptedException) {
                            //ignore
                        }
                        notifyListenConfig();
                    }
                }
            }, 0L, TimeUnit.MILLISECONDS);
            
        }

这里schedule执行的任务是基于基于BlockingQueue 的事件驱动模型长轮询:

这里的listenExecutebell 是一个BlockingQueue(阻塞队列),它充当了事件信号的角色。

整个逻辑可以分解为以下几个步骤:

  1. 启动任务executor.schedule(...) 负责在后台启动一个任务。这个任务是一个 while 循环,它会持续运行,直到线程池被关闭。
  2. 阻塞等待信号listenExecutebell.poll(5L, TimeUnit.SECONDS) 是这里的关键。这个方法会让当前线程进入阻塞状态,它会做两件事:
    • 等待:线程会在这里“休眠”,等待 listenExecutebell 队列中出现一个新元素。只要有新元素被放入队列,线程就会立即被唤醒。
    • 超时唤醒:如果队列在 5 秒内都没有新元素,poll() 方法会返回 null,线程也会被唤醒继续执行。
  3. 执行核心逻辑executeConfigListen() 方法是线程被唤醒后执行的真正逻辑。它负责:收集需要监听的配置:遍历所有已注册的监听器,收集 dataIdgrouptenant 等信息。发送 gRPC 请求:将这些信息打包成一个 gRPC 请求,发送给 Nacos 服务器,并等待服务器响应。完成后listenExecutebell.offer(bellItem)往阻塞队列放入一个元素,这样下次while循环可以poll获取继续执行。

这种基于 BlockingQueue 的设计,比简单的 while (true) 循环更加高效和灵活,因为它实现了生产者-消费者模型

  • 生产者:当一个新的配置监听器被添加(例如,调用 addListener() 方法)时,或者当 Nacos 服务器推送了配置变更的响应时,某个线程会将一个信号(如一个空对象)放入 listenExecutebell 队列中。
  • 消费者startInternal() 启动的线程就是消费者。它会一直等待队列中的信号。

这种模式带来了几个核心优势:

  • 高效利用 CPU:线程在 poll() 处等待时,不会占用 CPU 资源(不像 while 循环中带 sleep() 的方式)。它只在有新事件发生时才被唤醒,极大地节省了系统开销。
  • 响应速度更快:一旦有新的配置需要监听,或者收到了来自服务器的推送通知,线程可以立即被唤醒,而无需等待上一个轮询周期结束。
  • 优雅地处理并发BlockingQueue 是线程安全的,它能够可靠地处理多个线程同时添加事件信号的情况。

更多推荐