Nacos 2: Spring Boot Demo 实战

本章遵循"先讲清知识点,再给完整示例"的原则,逐步完成 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"

这带来了三个核心问题:

  1. 扩缩容问题:新增实例无法自动被调用方感知
  2. 故障摘除问题:某实例宕机,调用方依然会把请求发过去
  3. 维护成本:每次 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 的方式:

  1. 控制台 -> 命名空间 -> 找到对应行的"命名空间ID"列
  2. 通过 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 控制台操作,而应该:

  1. 配置文件存放在 Git 仓库(版本管理)
  2. 通过 CI/CD 流水线自动发布(走审批流程)
  3. 发布后自动验证

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节点集群,停掉一个节点后验证仍可用
相关推荐
用户622475758461 小时前
面试官问我:"如何实现你项目中的这块代码."我说:"看好了."
后端
土豆.exe1 小时前
Cast Attack:Java 中 Ghost Bits(幽灵比特)引发的新型安全威胁——Java 生态里被忽视的底层风险引发一系列绕过
java·python·安全
阿丰资源1 小时前
基于Spring Boot的美容院管理系统(附源码+数据库+文档)
数据库·spring boot·后端
shughui1 小时前
2026最新JDK版本选择及下载安装详细图文教程【windows、mac附安装包】
java·linux·开发语言·windows·jdk·mac
Wenzar_1 小时前
# D3.js实战进阶:从基础图表到交互式数据仪表盘的全流程构建在现代前端开发中,**数据可视化已成为提升用户体验的核心能力之一
java·javascript·python·信息可视化·ux
TE-茶叶蛋1 小时前
Spring自动配置分析
java·后端·spring
XiYang-DING1 小时前
【Java EE】锁策略、锁升级、锁消除和锁粗化
java·redis·java-ee
JAVA面经实录9171 小时前
Spring Boot + Spring AI 完整实战手册
人工智能·spring boot·spring·ai编程
wu8587734571 小时前
Java AI Harness 落地:拥抱框架还是回归本质?深度解析选型之道
java·人工智能·回归