1. 为什么我把配置从 application.yml 里搬到了 Nacos
先聊点实际的。做 Spring Boot 项目久了,你一定会遇到这几个让头疼的场景:接口需要临时改个参数,本地改完 commit、打包、重启,一套流程下来几分钟没了;多环境切换的时候配置文件散落一地,dev、test、prod 三个 yml 内容对不上,排查半天发现是漏改了一个;配置改了但没同步到所有实例,负载均衡一打过来,有的节点走了新配置,有的还是旧值——这种线上事故,出一次就够你长记性。
我就是在这种背景下开始用 Nacos 的。Nacos 是阿里巴巴开源的服务发现与配置管理平台,在 Spring Boot 项目里它最核心的价值就是两件事:把配置从应用里拿出来集中管理,以及改了配置不用重启服务就能生效。配置中心解决的不是某个具体业务功能问题,而是让整个项目的配置管理方式发生改变。
这篇博文我把从零开始用 Nacos 的完整链路写出来:安装、集成、动态刷新、多环境管理、踩坑排查。没有太多理论高谈阔论,大部分是在真实项目中跑过、验证过的经验。如果你正准备把项目里的配置迁到 Nacos,或者已经集成了但总遇到诡异问题,这篇应该能帮你省不少时间。
2. 安装与启动:先把 Nacos 跑起来再谈其他
2.1 下载版本怎么选
去 GitHub 的 nacos.io 下载页或者官方 release 页面拉包。版本选择上,我建议直接用 2.x 系列,比如 2.2.3 或者 2.3.x,因为 1.x 和 2.x 在通信协议上有很大差别,2.x 引入了 gRPC,长连接性能更好,服务端推送配置变更的实时性也更强。如果你的 Spring Boot 版本是 2.4 以下,用 1.x 也没问题,但新项目我不推荐再入 1.x 的坑。
下载的时候注意分清楚是nacos-server还是nacos-client。我们需要的是 server 包,解压之后目录结构大致如下:
nacos/ ├── bin/ ├── conf/ ├── data/ ├── logs/ └── target/bin目录存放启动脚本,conf目录里是application.properties和nacos-mysql.sql,前者是 Nacos 自身的服务端配置,后者是如果要持久化配置到 MySQL 需要初始化的表结构。
2.2 单机模式启动:Windows、macOS、Linux 三平台实操
Nacos 默认支持两种启动模式:standalone(单机模式)和cluster(集群模式)。学习阶段跑单机就够了,生产环境再考虑集群。
Windows 下启动是最省事的。进入bin目录,startup.cmd默认是集群模式,直接双击会报错,需要先改一下启动参数或者用命令指定:
startup.cmd -m standalonemacOS 和 Linux 下用startup.sh,同样加-m standalone参数:
sh startup.sh -m standalone启动成功的标志是看到日志输出Nacos started successfully。Nacos 默认监听 8848 端口,启动完成后打开浏览器访问http://localhost:8848/nacos,默认用户名密码都是nacos,进入控制台就能看到左侧菜单有配置管理、服务管理、命名空间等功能。
这里有个新手容易忽略的细节:Nacos 2.x 除了 8848,还会占用 9848 和 9849 两个端口。9848 是 gRPC 的主端口,客户端连接服务端走的就是它。如果你在服务器上部署,防火墙和安全组只放行 8848 是不行的,必须同时放行 9848,不然客户端会一直报连接超时。
2.3 生产环境:Docker 部署和 MySQL 持久化
用 Docker 跑 Nacos 是部署阶段的首选,因为环境隔离干净,升级回滚也方便。单机模式直接一行命令:
docker run -d --name nacos-server -p 8848:8848 -p 9848:9848 -e MODE=standalone nacos/nacos-server:v2.2.3但默认情况下 Nacos 用的是内嵌数据库 Derby,数据只存在容器里,容器一删配置就全没了。生产环境必须持久化到 MySQL。在conf目录下找到nacos-mysql.sql,先在你的 MySQL 实例里执行一遍建表,然后启动时通过环境变量指定数据库连接:
docker run -d --name nacos-server \ -p 8848:8848 \ -p 9848:9848 \ -e MODE=standalone \ -e SPRING_DATASOURCE_PLATFORM=mysql \ -e MYSQL_SERVICE_HOST=你的数据库地址 \ -e MYSQL_SERVICE_PORT=3306 \ -e MYSQL_SERVICE_DB_NAME=nacos_config \ -e MYSQL_SERVICE_USER=root \ -e MYSQL_SERVICE_PASSWORD=你的密码 \ nacos/nacos-server:v2.2.3MySQL 版本要注意,Nacos 官方推荐 5.7 以上的版本。之前遇到过有人用 MySQL 5.6 跑 2.x 版本,启动的时候报索引超长,折腾了半天就是版本兼容问题。
2.4 集群模式:三节点起步的部署思路
生产环境不能把鸡蛋放在一个篮子里。集群模式下至少起三个 Nacos 节点,同时必须搭配 MySQL 高可用方案。集群节点之间通过 Raft 协议选举 leader,客户端连接到任意节点都能正常工作。
配置集群的方式是修改conf/cluster.conf.example为cluster.conf,里面填上所有节点的 IP:端口:
10.0.0.1:8848 10.0.0.2:8848 10.0.0.3:8848每个节点用startup.sh启动,Nacos 会自动发现集群成员。这里我实测的经验是:集群模式踩坑最多的地方往往不在 Nacos 本身,而在网络。节点之间如果存在防火墙隔离,Raft 选举会不稳定,现象就是控制台服务列表时有时无,日志里疯狂输出raft相关的报错。排查的时候先确认三个节点之间 8848 和 9848 端口能不能互通,再去看 Nacos 日志。
3. Spring Boot 集成 Nacos:从依赖到第一份配置
3.1 Maven 依赖引入
Spring Boot 集成 Nacos 配置中心有两个选择:一个是 Nacos 官方提供的spring-cloud-starter-alibaba-nacos-config,一个是 Nacos 自己出的nacos-spring-boot-starter。如果你的项目用了 Spring Cloud,用前者;如果只是单纯的 Spring Boot 项目没有引入 Spring Cloud 全家桶,用后者。
我在实际项目中用的是 Spring Cloud Alibaba 体系,依赖长这样:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2021.0.1.0</version> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2021.0.1.0</version> </dependency>版本对应关系是这里最大的坑。Spring Cloud Alibaba 的每个版本都对应特定的 Spring Boot 和 Spring Cloud 版本,选错了启动的时候就会出现各种NoSuchMethodError或者ClassNotFoundException。别用最新版,用你当前 Spring Boot 版本对应的稳定版。我这边一个项目用的 Spring Boot 2.6.13,配的 Spring Cloud Alibaba 2021.0.1.0,跑了半年没有遇到过兼容性问题。Spring Boot 2.4 以下要用 2.2.x 版本的 Spring Cloud Alibaba。
3.2 配置文件拆分:bootstrap.yml 和 application.yml
集成 Nacos 之后,配置文件的职责要重新划分。bootstrap.yml负责描述"怎么连 Nacos",application.yml负责本地的、不适合放在配置中心的配置(比如一些本地调试开关)。
在bootstrap.yml里做如下配置:
spring: application: name: order-service cloud: nacos: config: server-addr: 127.0.0.1:8848 file-extension: yaml namespace: public group: DEFAULT_GROUPspring.application.name很重要,因为 Nacos 的配置查找规则是${prefix}-${spring.profiles.active}.${file-extension}。举个例子,应用名是order-service,没有配置 profile,那么启动时 Nacos 会去找一个 dataId 为order-service.yaml的配置。如果配置了spring.profiles.active=dev,则会优先加载order-service-dev.yaml。
这个规则理解透了,多环境配置管理就很清晰了:每个环境一个配置文件,在 bootstrap.yml 里切换 profile 就能加载对应环境的配置。
3.3 在 Nacos 控制台新建第一条配置
打开控制台,进入配置管理 -> 配置列表,点击右上角的加号。Data ID 填order-service.yaml,Group 保持DEFAULT_GROUP,配置格式选 YAML,命名空间先用默认的 public。
配置内容我建议从一行最简单的测试配置开始:
app: name: order-service version: v1.0.0发布之后回到 IDEA,写一个测试接口读取这个配置:
@RestController public class ConfigController { @Value("${app.name:}") private String appName; @Value("${app.version:}") private String version; @GetMapping("/config") public Map<String, String> getConfig() { Map<String, String> result = new HashMap<>(); result.put("appName", appName); result.put("version", version); return result; } }启动项目,访问/config,如果能返回配置中心里的值,说明整条链路已经通了。这个步骤值得你亲手做一遍,因为后面所有的动态刷新、多环境管理,都是建立在这一条通路之上的。
3.4 no spring.config.import 报错:Spring Boot 2.4+ 的适配问题
这里必须单独拎出来说一个高频报错。Spring Boot 2.4 之后,spring.cloud.nacos.config相关的配置在bootstrap.yml里默认不再生效,因为 Spring Cloud 的 bootstrap 上下文默认被关闭了。启动项目会看到这样的错误:
No spring.config.import property has been defined解决方法有两种。第一种是在pom.xml里加上 bootstrap 的依赖,把它重新开启:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>第二种是放弃 bootstrap.yml,直接在 application.yml 里配置 Nacos,然后通过spring.config.import引入:
spring: config: import: nacos:order-service.yaml cloud: nacos: config: server-addr: 127.0.0.1:8848两种方案我都试过,第一种更省事,团队协作时别人也更容易理解。第二种方案灵活一些,但要求成员对 Spring Boot 的 config data 机制有足够理解,不然容易踩坑。
4. 动态刷新的原理与实战:不用重启就能改配置
4.1 为什么 @Value 有时候不会自动更新
动态刷新是 Nacos 配置中心最吸引人的特性,但很多人集成之后发现"改了配置没有自动生效",于是开始怀疑 Nacos 是不是有问题。其实大多数情况是用法不对。
Nacos 的配置动态刷新依赖 Spring Cloud 的事件机制。Nacos 客户端会通过长连接监听服务端配置的变化,当某个 dataId 下的配置内容发生变更时,服务端会推送一个事件给客户端,客户端收到之后发布RefreshEvent。Spring Cloud 的RefreshEventListener监听这个事件,刷新Environment中的配置值,同时触发RefreshScope中 bean 的重建。
关键点来了:普通的使用@Value注入的字段,在 Environment 刷新后不会自动更新,因为@Value是在 bean 实例化时把值注入进去的,bean 没有被销毁重建,字段自然不会变。要让@Value字段跟着刷新,必须在类上加上@RefreshScope:
@RefreshScope @RestController public class ConfigController { @Value("${app.version:}") private String version; @GetMapping("/version") public String getVersion() { return version; } }加了@RefreshScope之后,Spring 会把该 bean 的实例缓存起来,收到刷新事件时销毁旧实例、创建新实例,重新触发@Value的注入逻辑。这就是动态刷新的真相。
4.2 什么场景适合用 @RefreshScope,什么场景不适合
不是所有配置都适合动态刷新。@RefreshScope适合作用于轻量级的、无状态的对象,比如配置参数对象、开关标记、限流阈值等。
不适合的场景有几类:
- 数据库连接池:刷新需要销毁旧连接、重建连接池,过程非常重,很容易出现连接中断或者连接池泄漏。如果要用,建议把连接池的配置单独做一套热更新逻辑,而不是简单地打
@RefreshScope。 - Spring Security 的配置:安全配置涉及过滤器链,如果动态重建,可能会在重建瞬间出现鉴权失效的空窗期。
- 线程池:线程池的工作线程一旦启动就不好动态调整,强行刷新可能造成任务丢失。
我的习惯是:能刷新的就刷新,不能刷新的宁可把它放到数据库里,通过业务接口热更新,或者接受重启发布,不要强行上@RefreshScope。
4.3 用 Nacos 监听器实现业务级配置实时生效
除了 Spring Cloud 的@RefreshScope机制,Nacos 本身提供了监听器 API,可以更细粒度地去感知配置变化。这种方式更接近底层的编程模型,适合需要自定义逻辑的场景。
@Component public class NacosConfigListener { @PostConstruct public void init() { try { String dataId = "order-service.yaml"; String group = "DEFAULT_GROUP"; ConfigService configService = NacosFactory.createConfigService("127.0.0.1:8848"); configService.addListener(dataId, group, new Listener() { @Override public Executor getExecutor() { return Executors.newSingleThreadExecutor(); } @Override public void receiveConfigInfo(String configInfo) { System.out.println("配置发生变化:" + configInfo); // 在这里写你的业务处理逻辑 } }); } catch (NacosException e) { e.printStackTrace(); } } }这种方式不依赖 Spring Cloud 的刷新机制,收到配置变更通知后你可以执行任何逻辑。比如一个功能开关,收到变更后将最新值写入一个全局变量,业务代码读取这个全局变量时就能立即看到新值。
4.4 实际案例:流量控制参数的动态调整
说一个我在项目中实际用过的场景。系统有个接口依赖外部接口,外部接口的限流阈值是 100 QPS,但对方随时可能因为促销活动调整这个限制。如果每次都要改配置文件然后重启,就太被动了。
我把这个阈值放到了 Nacos 配置中心:
external: api: rate-limit: 100代码里用@ConfigurationProperties绑定一个配置类:
@RefreshScope @Component @ConfigurationProperties(prefix = "external.api") public class ExternalApiProperties { private int rateLimit; public int getRateLimit() { return rateLimit; } public void setRateLimit(int rateLimit) { this.rateLimit = rateLimit; } }限流逻辑里读这个值。运营同事在 Nacos 控制台把rate-limit改成 200,点击发布,几秒之内限流器就按新阈值工作了。整个过程没有改动一行业务代码,也没有发布操作,体验确实比传统方式顺畅太多。
当然这里也暴露了一个问题:配置能够被远程修改,意味着配置的变更要有审计和权限控制。谁改的、什么时候改的、改之前是什么值,这些信息都应该留痕。Nacos 控制台自带配置历史版本和回滚功能,但也需要在团队制度上要求"改配置走审批"。
5. 命名空间、分组与多环境配置管理
5.1 从 public 到自定义命名空间:隔离才是重点
默认情况下所有配置都在 public 命名空间里。开发、测试、生产环境的配置如果全部放在 public 里,光靠 dataId 带环境后缀来区分,维护起来会非常混乱。
Nacos 的命名空间(namespace)提供了一个隔离机制。不同命名空间之间,配置数据完全隔离,互不可见。我通常的做法是:
dev命名空间:开发联调用test命名空间:测试环境用prod命名空间:生产环境用
在 Nacos 控制台的命名空间菜单里创建一个命名空间,系统会生成一个 namespace ID(一串 UUID)。然后在bootstrap.yml里指定:
spring: cloud: nacos: config: namespace: 你的namespaceID这里有个容易混淆的点:namespace在配置文件里填的是命名空间ID,不是命名空间名称。如果你在控制台创建了一个叫dev的命名空间,ID 是一串随机字符串,配置里就要填那串 ID。有人没注意这一点,填了名称,启动时一直报找不到命名空间。
5.2 分组:同一环境下的业务维度隔离
比命名空间小一层的隔离单位是分组(Group)。同一个命名空间下可以有多个分组,默认分组叫DEFAULT_GROUP。
我的实践经验是:用命名空间隔离环境,用分组隔离业务线。比如你在一个命名空间里同时管理订单和用户两个系统的配置,可以建ORDER_GROUP和USER_GROUP两个分组,这样即使 dataId 冲突了也不会互相影响。
在 Spring Boot 集成时,通过group属性指定要拉取哪个分组的配置:
spring: cloud: nacos: config: group: ORDER_GROUP5.3 shared-configs 和 extension-configs:公共配置的复用
一个企业里往往有很多微服务,每个服务都有数据库连接、Redis 连接等公共配置。如果每个服务的配置都单独维护一份,改了数据库密码就要挨个去改,非常容易漏。
Spring Cloud Alibaba 提供了shared-configs和extension-configs来解决公共配置的复用问题:
spring: cloud: nacos: config: shared-configs: ->telnet 127.0.0.1 8848 telnet 127.0.0.1 9848如果 9848 不通,即使 8848 通着,客户端也连不上,因为 2.x 的 gRPC 长连接走的是 9848。这个坑在第一次部署到云服务器时几乎必踩。
7.2 配置不生效:先分清"没加载"和"没刷新"
配置不生效是有两种情况的,排查思路完全不同:
- 启动时就没加载到 Nacos 的配置,应用用的是本地默认值。这类问题看启动日志,搜索
Located property source或者nacos相关日志,确认是否成功拉取了远程配置。如果发现日志里根本没有拉取动作,检查 bootstrap.yml 配置和依赖是否引入。 - 启动时加载了,但修改后不自动刷新。这类问题重点检查
@RefreshScope是否加了、shared 配置的 refresh 开关是否打开、是否有多个配置源互相覆盖。
一个非常隐蔽的问题:如果同一个 key 在本地 application.yml 和 Nacos 配置里都存在,本地配置的优先级更高,Nacos 里的配置不会覆盖本地。这样你在 Nacos 里怎么改都不会生效,因为被本地配置屏蔽了。排查时不要忽略本地文件。
7.3 Nacos 版本与 Spring Boot 版本兼容矩阵
回想一下我遇到过的兼容性问题:Spring Boot 2.4 遇到 spring-cloud-starter-alibaba-nacos-config 旧版本时 bootstrap 失效;Spring Boot 3.x 使用 Nacos 客户端时出现 javax 和 jakarta 命名空间冲突;Spring Cloud 2020.0 之后的版本在配置加载机制上做了大改。
最稳妥的做法是去 Spring Cloud Alibaba 官方文档查看版本对应关系表。这里给出我实际使用中验证过的一套:
| Spring Boot | Spring Cloud | Spring Cloud Alibaba |
|---|---|---|
| 2.3.x | Hoxton.SR8 | 2.2.5.RELEASE |
| 2.4.x | 2020.0.x | 2021.1 |
| 2.6.x | 2021.0.x | 2021.0.1.0 |
| 3.0.x | 2022.0.x | 2022.0.0.0-RC1 |
版本选型上不要盲目追求最新。生产环境稳定性优先,选择一个经过市场验证的稳定组合,不要频繁升级,除非有明确的新特性需求或者安全漏洞需要修复。
7.4 控制台能访问但客户端连不上
这个场景我也遇到过:浏览器访问http://IP:8848/nacos正常,但 Spring Boot 项目启动时报连接失败。
这里要理解控制台访问和客户端连接是两套不同的链路。控制台走的是 HTTP 接口,浏览器访问只需要 8848 端口通就行。客户端在 2.x 中会同时使用 8848(HTTP)和 9848(gRPC),如果 9848 不通,客户端无法注册长连接,就会出现这种"控制台正常但客户端异常"的现象。
还有一种情况是 Nacos 服务端所在机器的/etc/hosts里配置了主机名,客户端通过 IP 连接时可能触发反向解析问题。遇到这种问题,检查客户端日志中连接的服务端地址是否和实际地址一致。
7.5 配置修改了但灰度环境没生效
如果你有多个环境共用一个 Nacos 集群,只是用命名空间区分,那么要确认服务实例连接的是不是目标命名空间。有时候本地配置文件被 IDE 缓存或者 Maven 多模块打包时引用了错误的配置文件,导致服务实际连到了别的环境。
排查方法是调一个接口返回当前环境信息:
@RestController public class EnvController { @Value("${spring.cloud.nacos.config.namespace:}") private String namespace; @GetMapping("/env") public String getEnv() { return "namespace=" + namespace; } }这样能够直观地看到服务实际连接的命名空间,再和控制台的配置位置对比就能定位问题。
7.6 性能问题:配置多、客户端多的时候
当你的微服务数量多起来,每个服务都通过长连接连接到 Nacos,Nacos 服务端的连接数和网络开销会明显上升。我遇到过一个项目,两百多个服务实例全部连接同一个 Nacos 集群,gRPC 长连接数量过高,服务端偶尔出现连接被重置。
针对这种情况,有几个调整思路:
- 给 Nacos 服务端调大 JVM 内存和连接数限制。
- 客户端缓存配置值,减少对服务端的频繁访问。
- 把 Nacos 集群做成多机房部署,让客户端就近连接。
另外,客户端本身也有配置缓存机制。本地缓存文件默认在~/nacos/config目录下,如果服务端挂了,客户端可以读取本地缓存继续启动。不过要注意缓存文件的权限管理,避免敏感配置泄露。
8. 一个小技巧:Nacos 与其他组件的协同
最后分享一个实用技巧。很多项目同时使用 Nacos 和 Redis、数据库,这些组件的连接信息都会放在 Nacos 配置中心。有一个容易忽略的问题:Nacos 配置中心的刷新机制不会自动重建 Redis 连接池、数据库连接池这类重量级组件。
我建议采用"配置变更通知 + 手动重建"的模式来协同。比如数据库密码轮换时,通过 Nacos 的监听器捕获配置变更,然后调用数据源的重建逻辑,而不是依赖@RefreshScope。这样既实现了动态更新,又避免了连接池被隐式重建带来的风险。
对于配置中心的使用,我的经验总结下来就是:配置集中管理本身就是最大的收益,动态刷新是锦上添花,不要为了动态刷新把系统搞复杂。如果某个配置确实需要经常变更,值得做热更新;如果一个月都改不了一次,重启一次也没什么大不了。分清哪些配置需要热更新,哪些配置稳定不变,比机械地给所有配置都加动态刷新有意义得多。