系列导读
这是系列的最后一篇,我们把目光投向 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 会被触发:

  1. NacosConfigService 回调业务 Listener。

  2. Spring Cloud Alibaba 的 NacosConfigAutoConfiguration 注册的 NacosConfigListener 收到配置内容。

  3. 调用 NacosContextRefresher.refresh()

  4. ContextRefresher 发布一个 RefreshEventRefreshEventListener 监听到后销毁所有 @RefreshScope 的 Bean 缓存。

  5. 当这些 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.serviceConfigPersistServiceConfigCacheServiceLongPollingServiceAsyncNotifyService

  • server.service.raftRaftConsistencyServiceImplConfigSMRaftStore

  • server.controllerConfigController(HTTP API)

  • server.remote.grpcConfigPushGrpcServiceGrpcPushService

客户端(client 模块):

  • configNacosConfigServiceHttpAgentLongPollingRunnable

  • config.grpcNacosConfigGrpcClientConfigChangeNotifyStreamObserver

Spring Cloud Alibaba 整合:

  • NacosPropertySourceLocatorNacosContextRefresherNacosConfigAutoConfiguration

你可以从 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 的取舍、最终一致性的协议设计、事件驱动的推送架构、高度可插拔的扩展体系。理解这些,对于学习其他分布式中间件也大有裨益。

本系列到此完结,感谢你的阅读。如果对你有帮助,请点赞、收藏支持,也欢迎将系列分享给更多的朋友。如果有任何问题或希望我更新某些细节,请在评论区留言,我会尽力回复。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐