Nacos 第 8 篇:源码拆解:配置中心模块关键实现
系列导读
这是系列的最后一篇,我们把目光投向 Nacos 的另一半心脏——配置中心(Config)。在前面第 3 篇我们讲过长轮询、灰度发布和动态刷新的原理,第 5 篇剖析了 Raft 在配置中心的落地。今天我们会直接打开源码,把配置从发布到客户端热加载的整条链路“跑”一遍:配置在服务端如何存储、历史版本如何快照、变更事件怎样在集群中传播、客户端的监听回调链是怎么设计的、以及 @RefreshScope 到底如何收到通知并刷新 Bean。
读完这一篇,你将能够对 Nacos 配置中心的所有核心实现如数家珍。
一、配置存储模型:config_info 与 his_config_info
Nacos 配置中心的数据持久化依赖 MySQL(或内置 Derby),其核心表结构设计非常简洁,但支持了强大的版本管理和回滚功能。
1.1 主表 config_info
这张表保存当前生效的配置,关键字段如下:
CREATE TABLE config_info (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
data_id VARCHAR(255),
group_id VARCHAR(255),
tenant_id VARCHAR(128) DEFAULT '',
content LONGTEXT,
md5 VARCHAR(32),
gmt_create DATETIME,
gmt_modified DATETIME,
src_user VARCHAR(128), -- 发布用户
encrypted_data_key VARCHAR(255), -- 加密密钥
...
);
一个配置由 data_id + group_id + tenant_id 三元组唯一确定。content 存储完整配置文本,md5 是内容的 MD5 校验值,用于客户端快速判断是否需要更新。
1.2 历史表 his_config_info
每次配置变更,旧版本会被移入历史表:
CREATE TABLE his_config_info (
id BIGINT,
data_id VARCHAR(255),
group_id VARCHAR(255),
tenant_id VARCHAR(128),
content LONGTEXT,
md5 VARCHAR(32),
gmt_create DATETIME,
gmt_modified DATETIME,
src_user VARCHAR(128),
op_type VARCHAR(10), -- INSERT/UPDATE/DELETE/ROLLBACK
...
);
op_type 标识变更类型,回滚操作本质上是将历史版本的内容作为新版本再次发布,并生成一条 op_type='ROLLBACK' 的新记录。源码中 ConfigPersistService 负责与数据库交互,关键方法有:
-
insertOrUpdate():插入或更新config_info,同时将旧值移入his_config_info。 -
findConfigHistory():查询历史版本列表。 -
rollbackConfig():取历史内容重新发布。
1.3 内存缓存
数据库是持久化保证,但配置的读取不能每次都查库。Nacos 服务端在启动时会全量加载 config_info 到内存的 ConfigCacheService 中,它是一个 ConcurrentHashMap<String, CacheItem>,Key 为 data_id + group_id + tenant_id 拼接的字符串,Value 为 CacheItem,包含配置内容、MD5、最后修改时间、以及一个 isBeta 标记(如果存在灰度版本)。
当配置发生变更,数据库更新后,ConfigCacheService 同步更新内存缓存,并触发事件通知客户端。
二、一致性快照与持久化:Raft 状态机的实现
在第 5 篇我们知道了 Raft 协议的核心,现在看它在配置中心源码中的具体应用。
2.1 Raft 日志存储
Nacos 使用 SOFAJRaft 作为 Raft 引擎,日志持久化在本地文件系统(默认路径 $NACOS_HOME/data/raft/)。RaftStore 负责日志读写,基于 JRaft 的 SegmentLogStorage。
2.2 状态机 ConfigSM
Raft 的“应用端”是状态机,Nacos 实现为 ConfigSM(StateMachine),核心方法 onApply(iter) 在 Raft 提交日志后回调:
@Override
public void onApply(Iterator iter) {
while (iter.hasNext()) {
ByteBuffer data = iter.next().getData();
ConfigChangePacket packet = (ConfigChangePacket) Serializer.deserialize(data);
// 1. 持久化到数据库
configPersistService.updateOrInsert(packet);
// 2. 更新内存缓存
configCacheService.updateOrInsert(packet);
// 3. 发布事件,通知其他节点和客户端
NotifyCenter.publishEvent(new ConfigDataChangeEvent(packet.getDataId(), ...));
iter.next();
}
}
关键步骤:
-
configPersistService.updateOrInsert():更新config_info表,旧版移入his_config_info。 -
configCacheService.updateOrInsert():更新内存中的CacheItem。 -
NotifyCenter.publishEvent():发布一个ConfigDataChangeEvent,驱动通知流程。
这样,Raft 的强一致性保证了所有节点在应用日志后,数据库和内存缓存都与 Leader 完全一致,绝不出现脏读。
2.3 节点启动时的数据回放
当 Nacos 节点重启,JRaft 会自动回放所有已提交但可能未应用的状态机日志(snapshot + log replay),确保 ConfigSM.onApply() 将最新的配置应用到数据库和内存,实现崩溃恢复。
三、配置变更事件传播:从 NotifyCenter 到客户端
配置一旦在 Raft 状态机中生效,就需要尽快通知所有正在监听的客户端。这一过程围绕事件总线 NotifyCenter 展开。
3.1 服务端事件流
NotifyCenter 是 Nacos 内部的事件中心,基于经典的 发布-订阅 模式。配置变更事件 ConfigDataChangeEvent 发布后,有几个关键订阅者:
-
AsyncNotifyService:负责 HTTP 长轮询和 gRPC 长连接的通知。
-
DumpService:负责将最新配置定期同步到磁盘快照(容灾用)。
AsyncNotifyService 的核心逻辑:
@Override
public void onEvent(Event event) {
if (event instanceof ConfigDataChangeEvent) {
ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event;
String dataId = evt.dataId;
String group = evt.group;
String tenant = evt.tenant;
// 1. 唤醒所有 HTTP 长轮询挂起的客户端
List<LongPollingClient> clients = longPollingService.getClients(dataId, group, tenant);
for (LongPollingClient client : clients) {
client.sendResponse(evt.lastModifiedTime, evt.md5);
}
// 2. 通过 gRPC 推送
grpcPushService.pushConfigChange(dataId, group, tenant, evt.md5, evt.lastModifiedTime);
}
}
HTTP 长轮询:LongPollingClient 被唤醒后,会生成包含新 MD5 的 HTTP 响应返回给客户端。
gRPC 推送:通过双向流 Connection 发送 NotifyConfigRequest protobuf 消息。
3.2 集群节点间的事件同步
在 Raft 的体系下,ConfigDataChangeEvent 只在 Leader 的状态机中发布。Follower 节点也通过 Raft 日志应用同样的状态机更新,因此它们的 ConfigCacheService 和数据库与 Leader 同步。同时,每个节点可以独立通知连接在自己身上的客户端,无需跨节点传输“通知事件”。这就实现了“数据靠 Raft 一致,通知靠本地触发”的清晰解耦。
四、客户端监听回调链:长轮询与 gRPC 的双通道
客户端 SDK 监听配置变更有两种实现路径:HTTP 长轮询(1.x)和 gRPC 长连接(2.x)。下面结合源码看它们如何串起回调。
4.1 HTTP 长轮询客户端
客户端入口 ConfigService.addListener(),内部会启动一个 ConfigChangeListener 线程,循环发起长轮询请求。关键类 HttpAgent 和 LongPollingRunnable:
// 简化逻辑
public void run() {
while (!isStop) {
List<String> changedGroups = HttpAgent.httpPost(server + "/listener", ...);
for (String dataId : changedGroups) {
// 拉取最新配置
String content = HttpAgent.httpGet("/config", dataId, ...);
// 更新本地缓存
configCache.put(dataId, content);
// 回调 Listener
listener.receiveConfigInfo(content);
}
}
}
服务端 /listener 接口处理就是第 3 篇说的长轮询挂起机制,代码在 ConfigController.listener() -> LongPollingService.addLongPollingClient()。
4.2 gRPC 客户端(2.x 推荐)
Nacos 2.x 的 gRPC 模式下,客户端启动时会与服务器建立一条 BiRequestStream 双向流。服务端通过 GrpcConnection 持有这条流。当配置变更事件触发,GrpcPushService.pushConfigChange() 会构造一个 ConfigChangeNotifyRequest 消息,通过流推送给客户端。
客户端侧的 NacosConfigGrpcClient 有一个 StreamObserver,收到推送后:
public void onNext(ConfigChangeNotifyRequest request) {
// 拉取最新配置
String content = configRpcClient.getConfig(dataId, group, tenant);
// 触发监听器
configCache.put(dataId, content);
for (Listener listener : listeners) {
listener.receiveConfigInfo(content);
}
}
无论是哪种通信方式,最终都会调用到业务方注册的 Listener.receiveConfigInfo(),完成应用内逻辑处理。
五、配置热加载:@RefreshScope 与 Spring Cloud 整合
Spring Cloud Alibaba Nacos Config 将 Nacos 的配置变更无缝接入 Spring 生态,其中最常用的就是 @RefreshScope。
5.1 NacosPropertySource 注册
应用启动时,NacosPropertySourceLocator 从 Nacos 拉取配置并注册为 NacosPropertySource,放入 Spring 的 Environment 中。这使得我们可以用 @Value 注入配置。
5.2 配置变更的传播链
当 Nacos 客户端收到配置变更(无论是长轮询还是 gRPC),NacosContextRefresher 会被触发:
-
NacosConfigService回调业务 Listener。 -
Spring Cloud Alibaba 的
NacosConfigAutoConfiguration注册的NacosConfigListener收到配置内容。 -
调用
NacosContextRefresher.refresh()。 -
ContextRefresher发布一个RefreshEvent,RefreshEventListener监听到后销毁所有@RefreshScope的 Bean 缓存。 -
当这些 Bean 下次被访问时,重新从
Environment取值,即最新的配置。
关键源码在 NacosContextRefresher:
java
public void refresh() {
// 发布 RefreshEvent
applicationContext.publishEvent(new RefreshEvent(this, null, "Nacos config changed"));
}
RefreshScope 本身是 Spring Cloud 提供的通用机制,Nacos 只是通过事件触发它。
5.3 非 Spring 环境
对于不使用 Spring 的 Java 应用,Nacos 同样支持通过 ConfigService.addListener() 直接注册 Listener,在回调中自行解析配置并更新业务变量。这同样能实现“热加载”,只是少了自动刷新 Bean 的便利。
六、配置模块源码关键类索引
为了方便你深入阅读,这里给出 Nacos 2.x 配置中心的核心包和类:
服务端(config 模块):
-
server.service:ConfigPersistService、ConfigCacheService、LongPollingService、AsyncNotifyService -
server.service.raft:RaftConsistencyServiceImpl、ConfigSM、RaftStore -
server.controller:ConfigController(HTTP API) -
server.remote.grpc:ConfigPushGrpcService、GrpcPushService
客户端(client 模块):
-
config:NacosConfigService、HttpAgent、LongPollingRunnable -
config.grpc:NacosConfigGrpcClient、ConfigChangeNotifyStreamObserver
Spring Cloud Alibaba 整合:
-
NacosPropertySourceLocator、NacosContextRefresher、NacosConfigAutoConfiguration
你可以从 ConfigController.publishConfig() 开始,追踪一条配置发布如何经过 Raft,到通知客户端的全过程。
七、全系列总结
至此,我们完成了对 Nacos 从架构到原理、再到源码的系统性解读。让我们快速回顾这 8 篇的核心旅程:
| 篇目 | 主题 | 掌握内容 |
|---|---|---|
| 第 1 篇 | Nacos 架构总览 | 双引擎(AP+CP)、数据模型、插件化设计 |
| 第 2 篇 | 服务注册与发现 | 心跳机制、临时/持久实例、健康检查、自我保护 |
| 第 3 篇 | 配置中心机制 | 长轮询、灰度发布、动态刷新、@RefreshScope 原理 |
| 第 4 篇 | Distro 协议 | 一致性哈希、非对称复制、健康检查驱动、版本号 |
| 第 5 篇 | Raft 协议落地 | JRaft 引擎、Leader 选举、日志复制、状态机应用 |
| 第 6 篇 | 集群与高可用 | 跨机房部署、Nacos-Sync、就近路由、扩缩容 |
| 第 7 篇 | 服务发现源码 | 注册表结构、哈希环、责任链、gRPC 推送、注册全流程 |
| 第 8 篇 | 配置中心源码 | 存储模型、Raft 状态机、事件传播、客户端回调、热加载 |
如果你耐心读完了所有文章,相信你已经从“会用 Nacos”蜕变为“懂原理、能调优、敢看源码”的进阶开发者。Nacos 不仅仅是一个工具,它凝聚了分布式系统设计的诸多经典思想:AP 与 CP 的取舍、最终一致性的协议设计、事件驱动的推送架构、高度可插拔的扩展体系。理解这些,对于学习其他分布式中间件也大有裨益。
本系列到此完结,感谢你的阅读。如果对你有帮助,请点赞、收藏支持,也欢迎将系列分享给更多的朋友。如果有任何问题或希望我更新某些细节,请在评论区留言,我会尽力回复。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)