Nacos 配置中心深入
配置中心要解决"配置集中管理、变更即时生效、客户端动态感知"三个问题。Nacos 用长轮询 + MD5 比对实现低延迟变更通知。本文拆解服务端与客户端完整机制。
数据模型
配置唯一标识 = Namespace + Group + DataId- DataId:配置文件名,通常带后缀(
order-service.yml、common.properties) - Group:分组(默认
DEFAULT_GROUP) - Namespace:环境隔离
Namespace=prod, Group=DEFAULT_GROUP
└── order-service.yml → 订单服务配置
└── common.properties → 公共配置配置内容与版本
每个配置有:
- 内容(content)
- MD5(内容哈希,变更比对依据)
- 版本号(乐观锁,并发发布控制)
- 标签/灰度发布选项(Beta)
服务端存储与发布
发布流程
控制台/API 发布配置
├─ 1. 写入 MySQL(CP:JRaft 多节点同步)
├─ 2. 计算新 MD5
├─ 3. 更新内存缓存 + 通知长轮询等待者
└─ 4. 返回发布成功存储策略
| 场景 | 存储 |
|---|---|
| 单机演示 | 内嵌 Derby |
| 生产集群 | 外置 MySQL(所有节点共享同一数据库) |
配置数据走 CP 一致性(JRaft 写入 MySQL 主库),保证任何节点读到的配置一致。
长轮询机制(核心)
客户端"监听配置变更"不靠短轮询,也不靠纯推送,而是长轮询(Long Polling):
工作流程
客户端 ConfigService
│
├─ 1. 发起监听请求(含 DataId + 本地 MD5)
│
├─ 2. 服务端处理:
│ ├─ 立即比对 MD5
│ │ ├─ 不一致 → 立即返回新内容(客户端刷新)
│ │ └─ 一致 → 挂起连接(默认 30s 超时)
│ │
│ ├─ 等待期间配置被修改
│ │ └─ 立即返回新内容
│ │
│ └─ 30s 无变化 → 返回(客户端重新发起长轮询)
│
└─ 3. 循环往复时序图
客户端 服务端
│ 监听(DataId, md5) │
├────────────────────────▶│
│ │ 比对 MD5 相同
│ │ 挂起请求(最多 30s)
│ │
│ │ ← 配置被修改
│ 立即返回新配置 │
│◀────────────────────────│
│ 更新本地 + 触发 Listener │
│ │
│ 重新发起监听(新 MD5) │
├────────────────────────▶│长轮询 vs 其他方式
| 方式 | 实时性 | 服务端压力 | 说明 |
|---|---|---|---|
| 短轮询 | 差 | 高 | 客户端频繁请求 |
| 长轮询 | 好 | 低 | 挂起连接,变更即返回(Nacos 采用) |
| 推送 | 最好 | 中 | 需维持长连接状态 |
长轮询在"实时性与资源占用"间取得平衡:一次请求挂起 30 秒,只有 30 秒周期内的变更会立即响应。
MD5 比对机制
MD5 是变更检测的关键:
服务端计算:
配置内容 → MD5 哈希
客户端持有:
本地配置内容 → 本地 MD5
比对流程:
监听请求携带 本地 MD5
服务端与最新 MD5 比对
├─ 相同 → 挂起等待
└─ 不同 → 返回最新内容 + 新 MD5优点:只需传输哈希(几字节)而非全部内容,极大减少带宽;内容未变时连接几乎无开销。
Listener 回调机制
客户端注册监听器后,变更自动回调:
java
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
// 配置变更回调:拿到最新内容,刷新业务配置
orderProperties.refresh(configInfo);
}
@Override
public Executor getExecutor() {
return Executors.newSingleThreadExecutor(); // 回调线程池
}
});Spring Cloud 集成
java
@Configuration
@RefreshScope // 配置变更后重建 Bean
public class OrderConfig {
@Value("${order.discount}")
private String discount;
}NacosContextRefresher(spring-cloud-starter-alibaba-nacos-config)内部:
注册 Nacos Config Listener
└─ 收到变更 → 发布 RefreshEvent
└─ Spring 上下文刷新
└─ @RefreshScope Bean 重建,@Value 重新注入ConfigService 客户端源码
核心组件
ConfigService(客户端入口)
├─ ClientWorker 长轮询核心
│ ├─ checkUpdateDataIds:批量监听
│ └─ 长轮询任务调度
├─ NacosConfigService 配置读写封装
├─ LocalConfigInfoProcessor 本地缓存(.nacos 目录)
└─ ServerHttpAgent / gRPC 通信长轮询线程模型
java
// ClientWorker
public class ClientWorker {
private final ScheduledExecutorService executor; // 定时任务
private final LongPollingRunnable runnable; // 长轮询任务
class LongPollingRunnable implements Runnable {
@Override
public void run() {
// 1. 批量检查所有监听的配置 MD5
List<String> changedKeys = checkUpdateDataIds(cacheMap, ...);
// 2. 有变化 → 拉取新内容,触发 Listener
for (String dataId : changedKeys) {
String content = getConfigInner(dataId, group, ...);
cacheMap 更新;
for (Listener listener : listeners) listener.receiveConfigInfo(content);
}
// 3. 再次发起长轮询(循环)
executor.schedule(runnable, 1000, TimeUnit.MILLISECONDS);
}
}
}关键点
- 批量监听:一个长轮询请求携带多个 DataId,降低连接数
- MD5 先比对:
checkUpdateDataIds只回传变更的 DataId,内容变化才拉全量 - 本地缓存:
.nacos目录缓存配置,服务端不可用时兜底 - 重试:长轮询异常自动重试,保证监听不断
配置读取优先级
Spring Cloud Nacos 配置加载顺序(application 启动时):
1. Nacos 远端配置(DataId = {spring.application.name}-{profile}.yml)
2. Nacos 远端配置(DataId = {spring.application.name}.yml)
3. Nacos 远端共享配置(shared-configs)
4. 本地 application.yml优先级高者覆盖低者;同名配置以高优先级为准。
生产建议
- 配置按环境隔离:Namespace 区分 dev/prod,禁止跨环境混用
- 敏感配置加密:配置中心权限控制 + 内容加密(Nacos 2.x 支持密钥管理)
- 灰度发布:Beta 发布先灰度验证再全量
- 变更审计:开启操作审计,记录谁改了什么
- 客户端兜底:合理配置本地缓存,Nacos 故障时服务仍可启动
常见问题
- 配置变更为什么有时不生效? 检查 DataId/Group 是否匹配、
@RefreshScope是否标注、Profile 是否生效。 - 长轮询挂起多久? 默认 30s(
configLongPollTimeout),超时返回空再重新发起。 - MD5 比对会漏变更吗? 服务端变更后立即更新 MD5 并唤醒等待者,正常情况下不漏;极端故障时客户端兜底轮询。
- 本地缓存有什么用? Nacos 全部故障时,客户端仍可用
.nacos缓存启动,避免全链路瘫痪。