Nacos配置中心动态刷新:长轮询、MD5比对与@RefreshScope
Nacos配置中心动态刷新:长轮询、MD5比对与@RefreshScope
适读人群:使用Nacos配置中心、想理解动态刷新底层原理的Java开发者 | 阅读时长:约18分钟
开篇故事
有同学问我:Nacos 配置变了,客户端是怎么知道的?是 Nacos 服务端主动推送给客户端,还是客户端定时去拉取?
我说都不是——是长轮询。客户端向服务端发送一个 HTTP 请求,服务端挂起这个请求,等到配置变化或者超时(默认 30 秒)再返回。下一个轮询周期里,如果没有变化,服务端挂起 30 秒;如果有变化,立刻返回变更信息。
这种方式兼顾了实时性(有变化立刻推送)和效率(没变化不频繁请求)。
他又问:服务端怎么知道配置变没变?我说 MD5 比对。
今天把这套机制拆开来讲清楚。
一、Nacos 配置动态刷新整体流程
1.1 为什么不用 WebSocket 或 SSE?
长轮询的优势:
- 实现简单,基于 HTTP,无需特殊协议支持
- 对反向代理友好(Nginx 等可以正确处理)
- 连接数可控(每个客户端一个长轮询连接)
二、核心原理深度解析
2.1 客户端长轮询实现
Nacos 客户端的长轮询在 ClientWorker 里,核心是 LongPollingRunnable:
// ClientWorker.LongPollingRunnable(简化版)
class LongPollingRunnable implements Runnable {
private final int taskId;
@Override
public void run() {
List<CacheData> cacheDatas = new ArrayList<>();
// 收集本taskId负责的所有配置项
for (CacheData cacheData : cacheMap.values()) {
if (cacheData.getTaskId() == taskId) {
cacheDatas.add(cacheData);
}
}
// 先检查本地缓存文件(应对服务端故障)
List<String> inInitializingCacheList = new ArrayList<>();
for (CacheData cacheData : cacheDatas) {
if (!cacheData.isInitializing()) {
cacheData.checkListenerMd5(); // 比对MD5,触发监听器
} else {
inInitializingCacheList.add(cacheData.getDataId() + "+" + cacheData.getGroup());
}
}
// 向服务端发长轮询请求
List<String> changedGroupKeys = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
// 对于有变化的配置,拉取最新内容
for (String groupKey : changedGroupKeys) {
String[] key = GroupKey.parseKey(groupKey);
String dataId = key[0];
String group = key[1];
try {
// 拉取最新配置
String content = getServerConfig(dataId, group, tenant, 3000L);
// 更新本地缓存
CacheData cache = cacheMap.get(GroupKey.getKey(dataId, group, tenant));
cache.setContent(content);
// MD5会自动更新,下次轮询会用新MD5
} catch (Exception e) {
// 异常处理...
}
}
// 检查监听器(触发 @Value 和 @ConfigurationProperties 的刷新)
for (CacheData cacheData : cacheDatas) {
if (!cacheData.isInitializing()) {
cacheData.checkListenerMd5();
}
}
// 安排下一次长轮询
executorService.execute(this);
}
}2.2 MD5 比对机制
客户端向服务端发送的长轮询请求体:
dataId%02group%02tenant%01dataId%02group%02tenant%01...
每个配置项之间用 \x01 分隔,dataId/group/tenant 之间用 \x02 分隔服务端收到后:
- 解析出每个配置项的标识
- 查数据库(或缓存)中该配置的最新 MD5
- 与请求中的 MD5 比对
- 如果某个配置的 MD5 不同,立刻返回这个配置的标识
- 如果全部相同,挂起 29 秒,等待变化通知
三、完整代码实现
3.1 Spring Boot 集成 Nacos 配置
依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>bootstrap.yml(必须用 bootstrap.yml,在 ApplicationContext 创建前加载配置):
spring:
application:
name: order-service
profiles:
active: dev
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: dev-namespace-id # 命名空间(对应不同环境)
group: DEFAULT_GROUP
file-extension: yaml
# 主配置文件:order-service-dev.yaml
# 共享配置(多服务共用)
shared-configs:
- data-id: common-config.yaml
group: COMMON_GROUP
refresh: true # 是否支持动态刷新
# 扩展配置(优先级高于主配置)
extension-configs:
- data-id: order-service-datasource.yaml
group: DATABASE_GROUP
refresh: false # 数据库配置不动态刷新(重要!)3.2 @RefreshScope 使用与原理
package com.laozhang.nacos.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
/**
* 动态刷新的配置类
*
* @RefreshScope:告诉Spring这个Bean需要支持动态刷新
* 当 RefreshEvent 发布时,这个Bean会被销毁并重新创建(单例缓存失效)
*
* @ConfigurationProperties:从配置文件里绑定属性
*/
@Data
@Component
@RefreshScope
@ConfigurationProperties(prefix = "order.config")
public class OrderConfig {
/** 单次最大下单金额(支持动态调整)*/
private Long maxOrderAmount = 10000L;
/** 免运费阈值(支持动态调整)*/
private Long freeShippingThreshold = 99L;
/** 是否开启拼团功能(可动态开关)*/
private boolean groupBuyEnabled = true;
/** 限流阈值(支持动态调整)*/
private Integer rateLimitQps = 100;
}@RefreshScope 的实现原理:
@RefreshScope 是一个自定义 Scope,其内部实现是在 RefreshScope 类里维护一个缓存:
// RefreshScope 的核心逻辑(简化)
public class RefreshScope extends GenericScope {
// 缓存已创建的Bean
private Map<String, Object> cache = new ConcurrentHashMap<>();
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
// 从缓存获取,没有则创建
return cache.computeIfAbsent(name, k -> objectFactory.getObject());
}
/**
* 刷新所有 @RefreshScope 的Bean
* 清空缓存,下次访问时重新创建
*/
public void refreshAll() {
cache.clear(); // 清空缓存,下次访问重新从BeanFactory创建
// 发布 RefreshScopeRefreshedEvent
}
}3.3 在代码里使用动态配置
package com.laozhang.nacos.service;
import com.laozhang.nacos.config.OrderConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
// 注入的是 @RefreshScope 的代理,每次调用都会检查最新值
private final OrderConfig orderConfig;
public void createOrder(CreateOrderRequest request) {
// 使用动态配置,无需重启即可生效
if (request.getAmount() > orderConfig.getMaxOrderAmount()) {
throw new BusinessException("下单金额超过限额:" + orderConfig.getMaxOrderAmount());
}
if (!orderConfig.isGroupBuyEnabled() && request.isGroupBuy()) {
throw new BusinessException("拼团功能当前已关闭");
}
// ... 业务逻辑 ...
}
}3.4 监听配置变化(细粒度控制)
如果需要在配置变化时做额外处理(如重新初始化连接池),可以监听 @RefreshEvent:
package com.laozhang.nacos.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.Set;
/**
* 监听配置变化事件
* 可以在配置变化时做额外的处理
*/
@Slf4j
@Component
public class ConfigChangeListener {
@EventListener(EnvironmentChangeEvent.class)
public void onEnvironmentChanged(EnvironmentChangeEvent event) {
Set<String> changedKeys = event.getKeys();
log.info("[ConfigChange] 配置项变化: {}", changedKeys);
// 对特定配置项做处理
if (changedKeys.contains("order.config.rate-limit-qps")) {
log.info("[ConfigChange] 限流阈值变更,重新初始化限流器");
// 通知限流器更新
}
if (changedKeys.stream().anyMatch(key -> key.startsWith("datasource."))) {
log.warn("[ConfigChange] 数据源配置变更!需要手动重启服务");
// 发送告警
}
}
}四、踩坑实录
坑1:@Value 注入的值动态刷新不生效
症状:Nacos 配置改了,但 @Value("${order.config.maxAmount}") 注入的值没有更新。
根因:@Value 注入发生在 Bean 初始化时,此后字段值不会自动更新。@RefreshScope 的机制是销毁并重新创建 Bean,如果 Bean 没有 @RefreshScope,刷新时不会重建,@Value 就不会更新。
解决方案:在包含 @Value 的 Bean 上加 @RefreshScope:
@Component
@RefreshScope // 加这个!
public class SomeConfig {
@Value("${some.value}")
private String value;
}或者改用 @ConfigurationProperties(推荐,支持更复杂的配置结构)。
坑2:@RefreshScope 导致 Bean 重建时报错
症状:加了 @RefreshScope 后,配置刷新时偶发 NullPointerException。
根因:@RefreshScope 的 Bean 在重建期间,对它的调用可能拿到旧对象、新对象或者 null 状态。
解决:不要在高频调用路径(如过滤器、切面)上的 Bean 加 @RefreshScope,改为把配置读取放在单独的 @RefreshScope 配置类里,服务类持有这个配置类的引用。
坑3:Nacos 配置优先级问题
症状:明明在 Nacos 里配了 spring.datasource.url,但服务还是用了本地 application.yml 里的配置。
Nacos 配置优先级(从高到低):
1. 扩展配置(extension-configs,按顺序后面的优先)
2. 主配置({application-name}-{profile}.yaml)
3. 共享配置(shared-configs,按顺序后面的优先)
4. 本地 application.yml / application.properties
5. 本地 bootstrap.yml / bootstrap.properties确认配置文件名和 data-id 是否完全匹配(包括扩展名 .yaml vs .yml vs .properties)。
坑4:Spring Boot 2.4+ 默认禁用 bootstrap.yml
症状:升级 Spring Boot 2.4+ 后,Nacos 配置不加载了,服务启动时缺少必要配置。
根因:Spring Boot 2.4 开始,bootstrap.yml 默认不再自动加载,需要额外引入 spring-cloud-starter-bootstrap。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>或者把 Nacos 配置迁移到 application.yml(Spring Boot 2.4+ 支持在 application.yml 里配置 Nacos)。
五、总结与延伸
Nacos 动态配置刷新的三个关键点:
- 长轮询:客户端发 HTTP 请求,服务端挂起 30 秒等待变化,有变化立刻返回,平衡了实时性和效率
- MD5 比对:用配置内容的 MD5 值检测变化,避免传输全量内容
- @RefreshScope:通过自定义 Scope 实现 Bean 的懒销毁+重建,让
@Value和@ConfigurationProperties能感知配置变化
几个使用建议:
- 数据库连接、线程池等重量级资源的配置,不建议动态刷新(
refresh: false) - 功能开关、阈值、超时时间等轻量配置适合动态刷新
- 关键配置变更需要加监控告警(结合
EnvironmentChangeEvent)
