本章遵循"先讲清知识点,再给完整示例"的原则,逐步完成 Nacos 与 Spring Boot 的集成实战。
一、版本选择与依赖管理
知识点:为什么版本管理很重要
Nacos 客户端由 Spring Cloud Alibaba(SCA)提供,SCA 版本必须与 Spring Boot、Spring Cloud 版本严格对应。版本不匹配会导致:
- 启动时
ClassNotFoundException - 配置无法加载(静默失败)
- 自动装配类找不到
官方版本对应关系(2024年常用组合):
| Spring Boot | Spring Cloud | Spring Cloud Alibaba | Nacos Client |
|---|---|---|---|
| 3.2.x | 2023.0.x | 2023.0.1.x | 2.3.x |
| 3.1.x | 2022.0.x | 2022.0.0.x | 2.2.x |
| 2.7.x | 2021.0.x | 2021.0.6.x | 2.1.x |
最佳实践:用 BOM(Bill of Materials)统一管理,不要手动指定各依赖版本。
示例:pom.xml BOM 管理
xml
<properties>
<java.version>17</java.version>
<spring-boot.version>3.2.4</spring-boot.version>
<spring-cloud.version>2023.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.2</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud BOM -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud Alibaba BOM(包含 Nacos 版本) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Nacos 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Nacos 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Actuator:暴露 /actuator/refresh 端点 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
二、启动 Nacos Server
知识点:Nacos 的两种启动模式
| 模式 | 适用场景 | 存储 | 说明 |
|---|---|---|---|
| standalone(单机) | 本地开发、Demo | 内置嵌入式DB | 无需外部数据库,快速启动 |
| cluster(集群) | 生产环境 | MySQL | 多节点高可用,配置持久化 |
单机模式下,Nacos 使用内置的 Derby 数据库保存配置,数据存储在 nacos/data 目录下。重要提示:单机模式不适合生产,Derby 不支持多节点共享,且备份恢复不便。
端口说明:
| 端口 | 用途 |
|---|---|
| 8848 | HTTP 控制台和客户端 API |
| 9848 | gRPC 端口(Nacos 2.x 客户端使用,必须开放) |
| 9849 | gRPC 集群间通信 |
Nacos 2.x 客户端默认使用 gRPC(9848)通信,比 1.x 的 HTTP 长轮询更高效。若防火墙只开放了 8848 忘记开放 9848,客户端会连接失败。
示例:本地启动 Nacos
方式一:Docker(推荐本地开发)
bash
docker run --name nacos-standalone \
-e MODE=standalone \
-p 8848:8848 \
-p 9848:9848 \
-d nacos/nacos-server:v2.3.2
# 验证启动成功
curl -s http://localhost:8848/nacos/actuator/health | grep UP
方式二:本地解压包
bash
# 下载 nacos-server-2.3.2.tar.gz 并解压后
sh bin/startup.sh -m standalone
# 查看启动日志
tail -f logs/start.out
# 停止
sh bin/shutdown.sh
访问控制台:
text
http://localhost:8848/nacos
默认账号/密码:nacos / nacos
验证清单:
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 控制台可访问 | 浏览器打开 localhost:8848/nacos | 显示登录页面 |
| 健康状态 | curl localhost:8848/nacos/actuator/health |
返回 UP |
| 服务列表 | 控制台 > 服务管理 > 服务列表 | 空列表(还没注册服务) |
| 配置列表 | 控制台 > 配置管理 > 配置列表 | 空列表 |
三、配置中心:Spring Boot 读取 Nacos 配置
知识点 1:配置的三个定位维度
从 Nacos 拉取一份配置需要三个坐标精确定位,缺一不可:
text
Namespace(命名空间)
└── Group(分组)
└── Data ID(配置文件名)
Namespace :最高隔离级别,通常用于区分环境(dev / test / prod)。注意:填写时需要填 Namespace ID (一串 UUID),不是控制台显示的名称。默认 Namespace 的 ID 是空字符串 ""。
Group :组内隔离,通常用于区分业务线或项目。默认值是 DEFAULT_GROUP。
Data ID :具体一份配置的名称,通常命名为 应用名.yml(或 .properties、.json)。文件后缀决定了配置的解析格式。
知识点 2:spring.config.import 加载机制
Spring Boot 2.4+ 通过 spring.config.import 来声明从 Nacos 加载哪些配置:
yaml
spring:
config:
import: "nacos:order-service.yml" # 必须存在,不存在报错启动失败
import: "optional:nacos:order-service.yml" # 可选,不存在时用本地配置兜底
加载时机 :应用启动时,Spring Boot 在刷新 ApplicationContext 之前就会拉取 Nacos 配置,优先级高于本地 application.yml。
配置优先级(从高到低):
text
1. 命令行参数(--key=value)
2. 环境变量
3. Nacos 远程配置(spring.config.import 中的配置)
4. 本地 application.yml
5. 默认值
这意味着:如果 Nacos 和本地 application.yml 都有 server.port,Nacos 的值会覆盖本地值。
知识点 3:配置文件格式与 file-extension
Nacos 支持 YAML、Properties、JSON、XML 等格式。Spring Boot 集成时,客户端需要知道 Data ID 对应的格式:
- 如果 Data ID 有后缀(如
order-service.yml),会自动识别格式 - 如果 Data ID 没有后缀(如
order-service),需要通过file-extension指定
推荐:Data ID 始终带上文件后缀,避免歧义。
示例:配置中心 Demo
步骤 1:在 Nacos 控制台创建配置
| 字段 | 值 |
|---|---|
| Data ID | nacos-config-demo.yml |
| Group | DEFAULT_GROUP |
| Namespace | (默认,ID 为空) |
| 配置格式 | YAML |
配置内容:
yaml
demo:
message: "hello from nacos config"
max-retry: 3
timeout: 5000
步骤 2:项目 application.yml
yaml
server:
port: 8080
spring:
application:
name: nacos-config-demo
config:
# optional: 表示允许 Nacos 不可用时继续启动
import: "optional:nacos:nacos-config-demo.yml"
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
namespace: "" # 默认 Namespace
group: DEFAULT_GROUP
file-extension: yml # 配置格式(Data ID 有后缀时可省略)
management:
endpoints:
web:
exposure:
include: "refresh,health"
步骤 3:配置属性类
java
package com.example.nacosconfigdemo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 对应 Nacos 中的 demo.* 配置段
* 不需要 @RefreshScope,@ConfigurationProperties 会自动感知变化
* (需配合 spring-cloud-context 依赖)
*/
@Component
@ConfigurationProperties(prefix = "demo")
public class DemoProperties {
private String message = "default message"; // 提供默认值,防止 Nacos 不可用
private int maxRetry = 3;
private int timeout = 3000;
// getters and setters
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public int getMaxRetry() { return maxRetry; }
public void setMaxRetry(int maxRetry) { this.maxRetry = maxRetry; }
public int getTimeout() { return timeout; }
public void setTimeout(int timeout) { this.timeout = timeout; }
}
步骤 4:Controller
java
package com.example.nacosconfigdemo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConfigController {
private final DemoProperties demoProperties;
public ConfigController(DemoProperties demoProperties) {
this.demoProperties = demoProperties;
}
@GetMapping("/config")
public String config() {
return String.format("message=%s, maxRetry=%d, timeout=%d",
demoProperties.getMessage(),
demoProperties.getMaxRetry(),
demoProperties.getTimeout());
}
}
步骤 5:启动验证
bash
# 启动应用
mvn spring-boot:run
# 验证配置读取
curl http://localhost:8080/config
# 预期:message=hello from nacos config, maxRetry=3, timeout=5000
四、动态刷新:不重启应用更新配置
知识点 1:什么是动态刷新,原理是什么
Nacos 客户端通过长轮询 监听配置变更:客户端定期(默认最长30秒)向 Nacos Server 发起请求,询问配置是否有变化。Server 发现配置变更时立即返回,客户端重新拉取新配置并触发 Spring Cloud 的 RefreshEvent。
RefreshEvent 触发后,Spring Cloud Context 会重新绑定被标记为可刷新的 Bean:
text
Nacos 配置变更
-> 客户端长轮询感知
-> 重新拉取配置
-> 发布 RefreshEvent
-> Spring 重建 @RefreshScope Bean
-> @ConfigurationProperties 属性重新绑定
知识点 2:@RefreshScope vs @ConfigurationProperties 的刷新行为
| 方式 | 刷新机制 | 适用场景 | 注意点 |
|---|---|---|---|
@ConfigurationProperties |
自动感知,无需注解 | 一组结构化配置(推荐) | 需要 spring-cloud-context 在 classpath |
@Value + @RefreshScope |
RefreshEvent 触发 Bean 重建 | 少量简单配置 | Bean 重建有代价,代理对象可能有坑 |
@Value(无 @RefreshScope) |
不刷新 | 静态配置 | 应用启动后永远不变 |
@RefreshScope 原理:标注该注解的 Bean 被 Spring 包装成 CGLIB 代理对象。当 RefreshEvent 触发时,代理对象会销毁内部缓存的真实实例,下次调用时重新创建,从而读取到新配置。
重要陷阱:
java
// ❌ 错误:@Scheduled 等方法注入到其他 Bean 后,即使 @RefreshScope 也可能不生效
// 因为注入时拿到的是代理,方法调度器持有的引用在 Refresh 后不会更新
// ✅ 正确:优先使用 @ConfigurationProperties,它不依赖 Bean 重建
知识点 3:哪些配置不适合动态刷新
| 配置类型 | 能否动态刷新 | 原因 |
|---|---|---|
| 业务开关、阈值、文案 | ✅ 适合 | 无副作用,实时性要求高 |
| 数据库连接地址/池大小 | ⚠️ 谨慎 | 需要重建连接池,可能影响正在运行的事务 |
| MQ 地址、Topic | ⚠️ 谨慎 | 涉及消费者组重建,可能丢消息 |
server.port |
❌ 不生效 | 端口在 Server 启动时绑定,无法热更新 |
@Bean 的创建条件(@ConditionalOn*) |
❌ 不生效 | 条件在容器启动时求值,不会重新评估 |
spring.datasource.* |
❌ 不直接生效 | DataSource Bean 不是 @RefreshScope,需要额外处理 |
示例:动态刷新 Demo
在上一节的基础上,把 DemoProperties 加上 @RefreshScope(如果使用 @Value 的场景):
推荐方式(@ConfigurationProperties,自动支持刷新):
java
// 无需任何额外注解,@ConfigurationProperties 的属性在 RefreshEvent 后自动重新绑定
@Component
@ConfigurationProperties(prefix = "demo")
public class DemoProperties {
private String message;
private int maxRetry;
// ... getters/setters
}
备选方式(@Value + @RefreshScope):
java
@RestController
@RefreshScope // 整个 Bean 在 RefreshEvent 后被重建
public class ConfigController {
@Value("${demo.message:default}")
private String message;
@GetMapping("/config")
public String config() {
return message;
}
}
验证动态刷新流程:
bash
# 1. 查看当前配置
curl http://localhost:8080/config
# 输出:message=hello from nacos config, maxRetry=3, timeout=5000
# 2. 在 Nacos 控制台修改配置(把 message 改为 "hello updated"),点击发布
# 3. 等待约 1-3 秒,再次查看
curl http://localhost:8080/config
# 输出:message=hello updated, maxRetry=3, timeout=5000
# 不需要重启应用!
排查动态刷新不生效:
| 现象 | 检查点 |
|---|---|
| 控制台发布后配置没变化 | 1. 是否点击了"发布"(不是"保存")2. Data ID 和 Group 是否匹配 |
| 延迟超过1分钟才生效 | 检查长轮询配置,默认30秒超时 |
| 始终不生效 | 检查 Bean 是否有 @RefreshScope 或 @ConfigurationProperties |
| 部分字段生效部分不生效 | 检查是否有字段被 final 修饰(final 字段不可刷新) |
五、服务注册:Provider 注册到 Nacos
知识点 1:注册中心解决了什么问题
在没有注册中心的微服务架构中,服务地址是硬编码的:
yaml
# 传统方式(写死IP)
user-service:
url: "http://192.168.1.10:8081"
这带来了三个核心问题:
- 扩缩容问题:新增实例无法自动被调用方感知
- 故障摘除问题:某实例宕机,调用方依然会把请求发过去
- 维护成本:每次 IP 变化都需要修改所有调用方配置
注册中心的解决方案:
text
Provider 启动 -> 向 Nacos 注册(服务名 + IP + 端口)
Consumer 调用 -> 按服务名从 Nacos 查实例列表 -> 负载均衡选一个 -> 发起请求
Provider 下线 -> 心跳停止 -> Nacos 自动摘除 -> Consumer 不再调用
知识点 2:临时实例与永久实例
| 类型 | 注册方式 | 心跳 | 下线行为 | 适用场景 |
|---|---|---|---|---|
| 临时实例(ephemeral=true) | 客户端主动注册 | 客户端定时发心跳 | 心跳超时自动删除 | 普通 Spring Boot 微服务 |
| 永久实例(ephemeral=false) | 客户端注册或手工注册 | 服务端主动探测 | 不自动删除,需手工注销 | 静态配置的第三方服务 |
Spring Boot 微服务默认使用临时实例,这也是推荐的方式:服务实例异常时,心跳停止,Nacos 在 30 秒内自动摘除,Consumer 感知后不再调用异常实例。
知识点 3:服务元数据
注册时可以携带自定义元数据(key-value),用于:
- 灰度路由 :
version=v2标记新版本实例 - 地域感知 :
region=beijing标记区域 - 权重控制:通过元数据传递自定义权重标签
元数据在 Nacos 控制台服务详情页中可以查看,Consumer 侧也可以通过 SDK 读取。
知识点 4:注册时使用哪个 IP
在多网卡环境(容器、虚拟机、开发机多网卡)中,Nacos 客户端可能选择错误的 IP 注册,导致 Consumer 无法访问。
IP 选择优先级(默认规则):
text
1. spring.cloud.nacos.discovery.ip(显式配置,最高优先级)
2. spring.cloud.inetutils.preferred-networks(网段过滤)
3. 第一个非 loopback 非 docker0 的网卡 IP
生产排查命令:
bash
# 查看应用注册的 IP(在 Nacos 控制台服务详情页)
# 或通过 API 查询
curl "http://localhost:8848/nacos/v1/ns/instance/list?serviceName=nacos-provider-demo"
示例:Provider 服务注册 Demo
application.yml(provider):
yaml
server:
port: 8081
spring:
application:
name: nacos-provider-demo # 服务名,Consumer 按此名查询
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: "" # Namespace ID(空=默认)
group: DEFAULT_GROUP
# 多网卡时显式指定注册IP
# ip: 192.168.1.100
# 实例元数据(可选)
metadata:
version: v1
region: local
Provider 接口:
java
package com.example.nacosproviderdemo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProviderController {
// 注入当前实例端口,多实例运行时便于区分
@Value("${server.port}")
private String port;
@GetMapping("/provider/hello")
public String hello() {
return "hello from provider, port=" + port;
}
}
启动类(需要 @EnableDiscoveryClient,新版本可省略):
java
package com.example.nacosproviderdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
// @EnableDiscoveryClient // Spring Cloud 2020+ 后默认启用,可省略
public class NacosProviderDemoApplication {
public static void main(String[] args) {
SpringApplication.run(NacosProviderDemoApplication.class, args);
}
}
验证注册成功:
bash
# 1. 启动 provider
mvn spring-boot:run
# 2. 查询注册实例
curl "http://localhost:8848/nacos/v1/ns/instance/list?serviceName=nacos-provider-demo"
# 返回 JSON,hosts 数组中包含注册的实例信息
# 3. 控制台验证
# 打开 http://localhost:8848/nacos -> 服务管理 -> 服务列表
# 可以看到 nacos-provider-demo,实例数为 1
多实例验证(在不同端口启动两个 provider):
bash
# 终端1:默认端口8081
mvn spring-boot:run
# 终端2:覆盖端口为8091
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8091
# 查询实例列表,应该看到两个实例
curl "http://localhost:8848/nacos/v1/ns/instance/list?serviceName=nacos-provider-demo"
六、服务发现:Consumer 通过服务名调用 Provider
知识点 1:服务发现与客户端负载均衡
Consumer 拿到实例列表后,由客户端负载均衡器选择一个实例发起请求(区别于服务端负载均衡如 Nginx):
text
Consumer 代码调用服务名 "nacos-provider-demo"
-> Spring Cloud LoadBalancer 查询实例列表
-> 从 Nacos 本地缓存获取健康实例列表
-> 按策略(默认轮询 Round-Robin)选一个实例
-> 替换服务名为真实 IP:Port
-> 发起 HTTP 请求
客户端负载均衡的优点:
- 无单点,每个服务自己决策
- 可以实现复杂路由(按版本、按地域、按权重)
Spring Cloud LoadBalancer 默认策略 :RoundRobinLoadBalancer(轮询)。
知识点 2:OpenFeign 与服务发现的整合
OpenFeign 是声明式 HTTP 客户端,与 Spring Cloud LoadBalancer 整合后,通过服务名发起调用:
text
@FeignClient(name = "nacos-provider-demo")
-> 声明调用哪个服务
-> Feign 动态代理生成实现类
-> 调用时 LoadBalancer 解析服务名为 IP:Port
-> Feign 用 OkHttp/HttpClient 发送 HTTP 请求
OpenFeign 的核心注解:
| 注解 | 作用 |
|---|---|
@FeignClient(name) |
声明调用的服务名(必须与提供方的 spring.application.name 一致) |
@GetMapping / @PostMapping |
声明 HTTP 方法和路径(必须与提供方接口一致) |
@RequestParam / @PathVariable / @RequestBody |
参数传递方式 |
知识点 3:Consumer 与 Provider 必须在同一 Namespace 和 Group
服务发现的作用域由 Namespace + Group 共同决定:
text
Consumer(Namespace=prod, Group=TRADE_GROUP)
只能发现:Provider(Namespace=prod, Group=TRADE_GROUP)
Consumer(Namespace=prod, Group=DEFAULT_GROUP)
无法发现:Provider(Namespace=prod, Group=TRADE_GROUP) ← 找不到服务!
这是生产中最常见的问题之一:Namespace 和 Group 不一致,导致服务发现失败。
示例:Consumer 服务发现 Demo
pom.xml(consumer 额外依赖):
xml
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- LoadBalancer(Feign 需要) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
application.yml(consumer):
yaml
server:
port: 8082
spring:
application:
name: nacos-consumer-demo
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
namespace: "" # 必须与 provider 的 namespace 一致
group: DEFAULT_GROUP # 必须与 provider 的 group 一致
启动类(开启 Feign):
java
package com.example.nacosconsumerdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients // 扫描 @FeignClient 接口
@SpringBootApplication
public class NacosConsumerDemoApplication {
public static void main(String[] args) {
SpringApplication.run(NacosConsumerDemoApplication.class, args);
}
}
Feign 客户端接口:
java
package com.example.nacosconsumerdemo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* name 必须与 Provider 的 spring.application.name 完全一致
*/
@FeignClient(name = "nacos-provider-demo")
public interface ProviderClient {
/**
* 路径和方法必须与 Provider 接口一致
*/
@GetMapping("/provider/hello")
String hello();
}
Consumer Controller:
java
package com.example.nacosconsumerdemo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConsumerController {
private final ProviderClient providerClient;
public ConsumerController(ProviderClient providerClient) {
this.providerClient = providerClient;
}
@GetMapping("/consumer/hello")
public String hello() {
// 通过服务名调用,不需要知道 provider 的 IP 和端口
String result = providerClient.hello();
return "consumer -> provider: " + result;
}
}
端到端验证:
bash
# 1. 确保 Nacos 已启动
# 2. 启动 provider(端口8081)
cd nacos-provider-demo && mvn spring-boot:run &
# 3. 启动第二个 provider 实例(端口8091,验证负载均衡)
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8091 &
# 4. 启动 consumer
cd nacos-consumer-demo && mvn spring-boot:run &
# 5. 调用 consumer 接口
curl http://localhost:8082/consumer/hello
# 输出:consumer -> provider: hello from provider, port=8081
# 6. 多次调用,观察轮询负载均衡
for i in $(seq 1 6); do curl -s http://localhost:8082/consumer/hello; echo; done
# 预期:交替输出 port=8081 和 port=8091
排查服务发现失败:
| 现象 | 排查步骤 |
|---|---|
No instances available for nacos-provider-demo |
1. 检查 Nacos 控制台是否有该服务 2. 检查 Namespace/Group 是否一致 3. 检查 provider 是否正常运行 |
| 调用成功但负载不均衡 | 检查 LoadBalancer 策略,多次调用观察 port 变化 |
| provider 注册了但健康检查显示不健康 | 检查 provider 端口是否可达,心跳是否正常发送 |
| Feign 调用超时 | 检查 provider 注册的 IP 是否对 consumer 可达(多网卡问题) |
七、多环境配置:Namespace 隔离
知识点 1:为什么用 Namespace 而不是 Data ID 后缀区分环境
错误做法(把所有环境的配置放在同一个 Namespace):
text
DEFAULT Namespace:
order-service-dev.yml <- 开发环境
order-service-test.yml <- 测试环境
order-service-prod.yml <- 生产环境
问题:
- 权限无法精确控制(修改任一配置的人都能看到所有环境配置)
- 误操作风险高(改了 dev 的配置名,可能改成 prod)
- 应用配置复杂(需要在代码里根据环境拼接不同的 Data ID)
正确做法(Namespace 隔离环境):
text
dev Namespace (ID: dev-ns-id):
order-service.yml <- 开发环境的配置
test Namespace (ID: test-ns-id):
order-service.yml <- 测试环境的配置(同名,不同内容)
prod Namespace (ID: prod-ns-id):
order-service.yml <- 生产环境的配置
优点:
- 同一个 Data ID,不同环境,通过 Namespace 隔离
- 可以按 Namespace 授权,生产环境只有少数人可操作
- 应用代码和配置文件不变,只需切换 Namespace ID(通常通过环境变量注入)
知识点 2:Namespace ID 与 Namespace Name
Nacos 控制台创建 Namespace 时会自动生成一个 UUID 格式的 Namespace ID:
text
Namespace 名称(控制台显示): prod
Namespace ID(实际填写): a1b2c3d4-e5f6-7890-abcd-ef1234567890
重要 :在 application.yml 中必须填写 Namespace ID,不是 Namespace 名称。填写名称不会报错,但会连接到错误的 Namespace(或默认 Namespace)。
获取 Namespace ID 的方式:
- 控制台 -> 命名空间 -> 找到对应行的"命名空间ID"列
- 通过 API:
curl http://localhost:8848/nacos/v1/console/namespaces
知识点 3:环境变量注入 Namespace,实现一套镜像多环境复用
yaml
# application.yml
spring:
cloud:
nacos:
server-addr: ${NACOS_ADDR:localhost:8848}
config:
namespace: ${NACOS_NAMESPACE:} # 从环境变量读取,默认为空(Default Namespace)
group: DEFAULT_GROUP
discovery:
namespace: ${NACOS_NAMESPACE:}
启动时注入:
bash
# 本地开发(使用 dev Namespace)
export NACOS_NAMESPACE=dev-ns-id
mvn spring-boot:run
# 生产部署(使用 prod Namespace)
NACOS_NAMESPACE=prod-ns-id java -jar app.jar
# Kubernetes 中(通过 ConfigMap 注入)
env:
- name: NACOS_NAMESPACE
valueFrom:
configMapKeyRef:
name: app-env-config
key: nacos.namespace.id
示例:多环境配置 Demo
步骤 1:在 Nacos 控制台创建两个 Namespace
| Namespace 名称 | 用途 |
|---|---|
| dev | 开发环境 |
| prod | 生产环境 |
创建后记录各自的 Namespace ID。
步骤 2:在两个 Namespace 下创建相同 Data ID 但不同内容的配置
在 dev Namespace 下创建 multi-env-demo.yml:
yaml
demo:
env: development
db-url: "jdbc:mysql://dev-db:3306/dev_db"
feature-new-ui: true
在 prod Namespace 下创建 multi-env-demo.yml:
yaml
demo:
env: production
db-url: "jdbc:mysql://prod-db:3306/prod_db"
feature-new-ui: false
步骤 3:应用配置
yaml
server:
port: 8083
spring:
application:
name: multi-env-demo
config:
import: "optional:nacos:multi-env-demo.yml"
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
namespace: ${NACOS_NAMESPACE:} # 通过环境变量注入
group: DEFAULT_GROUP
步骤 4:验证
bash
# 使用 dev Namespace
NACOS_NAMESPACE=<dev-namespace-id> mvn spring-boot:run
curl http://localhost:8083/config
# 输出:env=development, feature-new-ui=true
# 使用 prod Namespace(换个端口避免冲突)
NACOS_NAMESPACE=<prod-namespace-id> mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8084
curl http://localhost:8084/config
# 输出:env=production, feature-new-ui=false
八、共享配置与扩展配置
知识点 1:为什么需要共享配置
在微服务架构中,多个服务通常共用相同的基础设施配置:
text
订单服务、用户服务、商品服务 都需要:
- 数据库连接池配置(主库/从库地址、连接数)
- Redis 配置(地址、密码、连接池)
- MQ 配置(Broker 地址、Topic 名称)
如果每个服务都维护一份相同的配置,当数据库 IP 变更时,需要逐一修改每个服务的配置,容易遗漏。
共享配置:把公共配置抽取到单独的 Data ID,多个服务引用同一份。
知识点 2:shared-configs vs extension-configs
| 类型 | 用途 | 覆盖优先级 |
|---|---|---|
shared-configs |
多服务共享的公共配置(如 common-db.yml) | 低(会被应用自身配置覆盖) |
extension-configs |
某个服务的扩展补充配置(如灰度规则、特殊开关) | 中(高于 shared,低于主配置) |
| 应用主配置(Data ID = 应用名.yml) | 当前服务的核心配置 | 最高 |
完整优先级(从高到低):
text
1. 应用主 Data ID(spring.application.name + file-extension)
2. extension-configs(列表中靠后的优先级更高)
3. shared-configs(列表中靠后的优先级更高)
4. 本地 application.yml
知识点 3:共享配置的 Group 建议单独管理
text
业务服务配置 Group: DEFAULT_GROUP
order-service.yml
user-service.yml
公共配置 Group: INFRA_GROUP(单独分组,便于权限控制)
common-db.yml
common-redis.yml
common-mq.yml
示例:共享配置 Demo
在 Nacos 控制台创建公共配置(INFRA_GROUP 分组)
common-db.yml(Group: INFRA_GROUP):
yaml
spring:
datasource:
url: "jdbc:mysql://shared-db:3306/mydb"
username: app_user
pool:
maximum-pool-size: 20
minimum-idle: 5
common-redis.yml(Group: INFRA_GROUP):
yaml
spring:
data:
redis:
host: shared-redis
port: 6379
timeout: 2000ms
application.yml(引用共享配置):
yaml
server:
port: 8085
spring:
application:
name: order-service
config:
import:
- "optional:nacos:order-service.yml" # 应用主配置(最高优先级)
- "optional:nacos:common-db.yml?group=INFRA_GROUP" # 共享DB配置
- "optional:nacos:common-redis.yml?group=INFRA_GROUP" # 共享Redis配置
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
namespace: ""
group: DEFAULT_GROUP
# 也可以通过 shared-configs 方式声明
shared-configs:
- data-id: common-db.yml
group: INFRA_GROUP
refresh: true # 是否参与动态刷新
- data-id: common-redis.yml
group: INFRA_GROUP
refresh: true
extension-configs:
- data-id: order-gray-rule.yml # 订单服务专属的灰度规则
group: DEFAULT_GROUP
refresh: true
九、常见问题排查清单
9.1 配置类问题
| 问题 | 根本原因 | 解决方法 |
|---|---|---|
| 应用启动时拉不到配置 | Data ID/Group/Namespace 不匹配 | 三个维度逐一核对,注意 Namespace 填 ID 不填名称 |
| 配置修改后不生效 | 未发布 / Bean 不支持刷新 | 确认控制台点击"发布";检查 Bean 是否有 @ConfigurationProperties 或 @RefreshScope |
| 读取到旧配置 | 本地缓存未更新 | 检查长轮询是否正常,查看客户端日志 |
| optional:nacos 启动正常但没读到配置 | Data ID 不存在或网络不通 | 去掉 optional: 让错误暴露,再定位 |
| 配置内容乱码 | 字符集不匹配 | 确保 Nacos 控制台保存时格式为 UTF-8 |
9.2 服务注册发现类问题
| 问题 | 根本原因 | 解决方法 |
|---|---|---|
| 控制台看到服务但 Consumer 找不到 | Namespace/Group 不一致 | 检查 consumer 和 provider 的 discovery 配置 |
| 注册的 IP 不可达 | 多网卡选择了内网/容器 IP | 显式配置 spring.cloud.nacos.discovery.ip |
| 实例频繁上下线 | 心跳超时或网络抖动 | 检查网络,适当调整心跳参数 |
| 负载均衡不均匀 | 部分实例权重为 0 | 检查 Nacos 控制台实例权重设置 |
| Feign 调用 404 | 路径不匹配 | 确保 @FeignClient 方法路径与 Provider 完全一致 |
| 服务下线后还偶尔被调用 | 客户端实例列表缓存未及时更新 | 正常现象(最多30秒),可配置心跳超时缩短感知时间 |
9.3 版本兼容类问题
| 问题 | 表现 | 解决方法 |
|---|---|---|
| 版本不匹配 | ClassNotFoundException、Bean注入失败 | 使用 BOM 统一管理版本,参考官方版本矩阵 |
| 9848 端口未开放 | 2.x 客户端连接失败 | 防火墙/安全组开放 9848 端口 |
| bootstrap.yml 不生效 | Spring Boot 2.4+ 默认不加载 | 改用 spring.config.import,或添加 spring-cloud-starter-bootstrap 依赖 |
十、专家级 Demo:生产实战场景
Demo 6:自定义配置监听器
知识点
有些场景需要在配置变更时执行自定义逻辑(不只是属性注入),例如:
- 重建数据库连接池
- 刷新规则引擎
- 发送告警通知
@ConfigurationProperties 只负责属性绑定,不能执行自定义逻辑。这时需要使用 Nacos 原生配置监听器:
text
configService.addListener(dataId, group, listener)
-> 当指定 dataId 的配置变更时
-> 回调 listener.receiveConfigInfo(newContent)
-> 在回调中执行自定义逻辑
注意:监听器回调在 Nacos 内部线程中执行,如果回调耗时较长,应异步处理,避免阻塞心跳线程。
java
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@Slf4j
@Component
public class RateLimitConfigListener {
private final NacosConfigManager nacosConfigManager;
// 异步执行,避免阻塞 Nacos 线程
private final Executor executor = Executors.newSingleThreadExecutor();
public RateLimitConfigListener(NacosConfigManager nacosConfigManager) {
this.nacosConfigManager = nacosConfigManager;
}
@PostConstruct
public void init() throws Exception {
nacosConfigManager.getConfigService().addListener(
"rate-limit-rule.yml",
"DEFAULT_GROUP",
new Listener() {
@Override
public Executor getExecutor() {
return executor; // 异步执行
}
@Override
public void receiveConfigInfo(String configInfo) {
log.info("限流规则变更,新配置内容: {}", configInfo);
// 解析 configInfo,更新内存中的限流规则
updateRateLimitRules(configInfo);
}
}
);
log.info("限流规则配置监听器已注册");
}
private void updateRateLimitRules(String configInfo) {
// 实际项目中:用 Jackson/SnakeYAML 解析配置,更新规则引擎
log.info("限流规则已更新");
}
}
Demo 7:CI/CD 自动发布配置脚本
知识点
生产环境配置变更不应由开发手动登录 Nacos 控制台操作,而应该:
- 配置文件存放在 Git 仓库(版本管理)
- 通过 CI/CD 流水线自动发布(走审批流程)
- 发布后自动验证
Nacos 提供 Open API,可通过 HTTP 接口完成配置发布:
bash
#!/bin/bash
# ci-deploy-config.sh
NACOS_ADDR="${NACOS_ADDR:-http://nacos-lb:8848}"
NAMESPACE="${NACOS_NAMESPACE}"
DATA_ID="${1}" # 第一个参数:Data ID
CONFIG_FILE="${2}" # 第二个参数:配置文件路径
# 获取访问 Token
TOKEN=$(curl -s -X POST "${NACOS_ADDR}/nacos/v1/auth/login" \
-d "username=${NACOS_USER}&password=${NACOS_PASS}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['accessToken'])")
[ -z "$TOKEN" ] && { echo "ERROR: 获取 Token 失败"; exit 1; }
# 读取配置内容
CONTENT=$(cat "${CONFIG_FILE}")
# 发布配置
RESULT=$(curl -s -X POST "${NACOS_ADDR}/nacos/v1/cs/configs" \
-H "Authorization: Bearer ${TOKEN}" \
--data-urlencode "tenant=${NAMESPACE}" \
--data-urlencode "dataId=${DATA_ID}" \
--data-urlencode "group=DEFAULT_GROUP" \
--data-urlencode "content=${CONTENT}" \
--data-urlencode "type=yaml")
[ "$RESULT" = "true" ] && echo "SUCCESS: ${DATA_ID} 发布成功" || { echo "ERROR: ${RESULT}"; exit 1; }
Demo 8:Docker Compose 本地集群演练
知识点
本地验证集群行为(节点宕机、Leader 切换)需要启动多个 Nacos 节点。Docker Compose 是最便捷的方式。
集群节点通过 NACOS_SERVERS 环境变量互相发现,通过共享 MySQL 数据库同步配置数据。
yaml
# docker-compose-nacos-cluster.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: nacos_config
MYSQL_USER: nacos
MYSQL_PASSWORD: nacos123
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
nacos1:
image: nacos/nacos-server:v2.3.2
environment:
MODE: cluster
NACOS_SERVERS: "nacos1:8848 nacos2:8848 nacos3:8848"
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: mysql
MYSQL_SERVICE_DB_NAME: nacos_config
MYSQL_SERVICE_USER: nacos
MYSQL_SERVICE_PASSWORD: nacos123
NACOS_AUTH_ENABLE: "true"
NACOS_AUTH_TOKEN: VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg5
ports:
- "8848:8848"
- "9848:9848"
depends_on:
mysql:
condition: service_healthy
nacos2:
image: nacos/nacos-server:v2.3.2
environment:
MODE: cluster
NACOS_SERVERS: "nacos1:8848 nacos2:8848 nacos3:8848"
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: mysql
MYSQL_SERVICE_DB_NAME: nacos_config
MYSQL_SERVICE_USER: nacos
MYSQL_SERVICE_PASSWORD: nacos123
NACOS_AUTH_ENABLE: "true"
NACOS_AUTH_TOKEN: VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg5
ports:
- "8849:8848"
- "9849:9848"
depends_on:
mysql:
condition: service_healthy
nacos3:
image: nacos/nacos-server:v2.3.2
environment:
MODE: cluster
NACOS_SERVERS: "nacos1:8848 nacos2:8848 nacos3:8848"
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: mysql
MYSQL_SERVICE_DB_NAME: nacos_config
MYSQL_SERVICE_USER: nacos
MYSQL_SERVICE_PASSWORD: nacos123
NACOS_AUTH_ENABLE: "true"
NACOS_AUTH_TOKEN: VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg5
ports:
- "8850:8848"
- "9850:9848"
depends_on:
mysql:
condition: service_healthy
volumes:
mysql-data:
启动与验证:
bash
# 启动集群
docker compose -f docker-compose-nacos-cluster.yml up -d
# 等待30秒左右,查看集群状态
curl -s http://localhost:8848/nacos/v1/ns/operator/servers | python3 -m json.tool
# 预期看到3个节点,状态为 UP
# 故障演练:停掉一个节点
docker stop nacos-knowledge-system-nacos1-1
# 验证:集群仍然可用(剩余2/3节点满足 Raft 多数派)
curl -s http://localhost:8849/nacos/v1/ns/operator/servers
# 恢复节点
docker start nacos-knowledge-system-nacos1-1
十一、本章练习任务
| 练习 | 目标 | 验收标准 |
|---|---|---|
| 练习 1 | 配置中心基础 | 修改 Nacos 配置后,接口在不重启情况下返回新值 |
| 练习 2 | 多实例负载均衡 | 启动 2 个 provider,多次调用 consumer 观察轮询 |
| 练习 3 | Namespace 隔离 | 创建 dev/prod 两个 Namespace,同名配置返回不同内容 |
| 练习 4 | 共享配置 | 把数据库配置抽到 common-db.yml,多个服务共同加载 |
| 练习 5 | 故障恢复 | 停掉 provider,观察 consumer 的报错;重启 provider 后自动恢复 |
| 练习 6 | 自定义监听 | 实现一个监听配置变更后打印日志的 Listener |
| 练习 7 | 集群演练 | 用 Docker Compose 启动3节点集群,停掉一个节点后验证仍可用 |