微服务

配置热更新:改配置不重启服务

把配置写死在代码或打包进镜像是常见的做法,但改一个限流阈值就要重新发布,成本太高。这篇文章讲清配置中心的两种接入方式、动态刷新的原理,以及哪些配置适合热更新。

2026-03-13 约 10 分钟 3998 字 微服务 Nacos 配置中心 Spring

一、一个很常见的低效场景

线上的限流阈值设成了 1000 QPS,大促前想调到 2000。如果配置写在 application.yml 里并打进了镜像,流程是这样的:

改配置文件 → 提交代码 → 走 CI 构建 → 打新镜像 → 推镜像仓库
→ 滚动重启所有实例 → 观察监控

为了改一个数字,走完了一整套发布流程,耗时十几分钟到半小时,期间还有发布风险。

配置中心的第一个价值就是消灭这个流程:改完配置,服务自动生效,不重启、不发布。

二、配置中心的两个职责

职责 说明
集中管理 所有服务的配置存在一处,不用去每台机器上翻文件
动态刷新 配置变更后,服务自动感知并应用,无需重启

另外还有几个附带好处:

  • 环境隔离:开发/测试/生产用同一个 key,不同的 namespace 或 group
  • 权限管控:谁能改哪份配置,有审计记录
  • 版本回滚:改错了可以回退到上一版本
  • 灰度发布:新配置先推给部分实例

常见的配置中心实现:Nacos、Apollo、Consul、Spring Cloud Config。

三、两种接入方式

Spring 生态下,让配置支持动态刷新有两种主流写法。

方式一:@RefreshScope + @Value

在使用了 @Value 的类上,加一个 @RefreshScope 注解:

@RestController
@RefreshScope          // ← 关键注解
public class OrderController {

    @Value("${order.timeout.minutes:30}")
    private int timeoutMinutes;

    @Value("${rate.limit.qps:1000}")
    private int limitQps;

    @GetMapping("/order/timeout")
    public int getTimeout() {
        return timeoutMinutes;
    }
}

配置变更后,@RefreshScope 标注的 Bean 会被销毁并重新创建,重新从配置中心注入新的值。

原理:@RefreshScope 的 Bean 实际是一个代理对象,真正的实例被缓存在 scope 里。收到刷新事件时,清空该 scope 的缓存,下次访问就会重新创建实例,从而拿到新配置。

注意它的副作用:Bean 被重建意味着实例状态会丢失。如果这个 Bean 里持有连接池、缓存、计数器之类的状态,重建会带来问题。所以 @RefreshScope 通常只用在无状态的配置读取类上。

方式二:@ConfigurationProperties + @RefreshScope

更适合一组相关配置的场景:

@Component
@RefreshScope
@ConfigurationProperties(prefix = "order")
public class OrderProperties {

    /** 订单超时时间(分钟) */
    private int timeoutMinutes = 30;

    /** 是否开启自动取消 */
    private boolean autoCancelEnabled = true;

    /** 单用户最大未支付订单数 */
    private int maxUnpaidOrders = 5;

    // getter / setter
}

使用时直接注入这个类:

@Service
public class OrderService {

    private final OrderProperties props;

    public OrderService(OrderProperties props) {
        this.props = props;
    }

    public void checkTimeout(Order order) {
        if (!props.isAutoCancelEnabled()) return;
        // ...
    }
}

两种方式的对比:

@Value @ConfigurationProperties
适合场景 零散的单个配置项 一组相关配置
类型安全 弱(需要 @Value("${x:默认值}") 兜底) 强(绑定到字段类型)
默认值 写在注解里 写在字段初始化
松散绑定 不支持 支持(timeout-minutes 映射到 timeoutMinutes)
校验 无 可配合 @Validated + JSR-303 注解
配置项多时 类里到处是 @Value,难维护 集中在一个类里,清晰

配置项超过三五个,建议用 @ConfigurationProperties。

四、配置变更的完整链路

以 Nacos 为例,一次配置修改的传播过程:

① 运维在 Nacos 控制台改配置 → 发布

② Nacos 服务端记录变更,并推送给所有订阅了该配置的客户端
   (长轮询机制:客户端保持一个长连接,服务端有变更时立即响应)

③ 客户端收到变更通知,拉取最新配置内容

④ 客户端发布 RefreshEvent 事件

⑤ Spring 容器监听该事件:
   - 销毁 @RefreshScope 标注的 Bean
   - 重新创建 Bean,注入新配置值

⑥ 业务代码下次访问时,拿到的是新配置

关键点在第 ② 步的"推送":Nacos 用的是长轮询(long polling),不是客户端定时拉取。

机制 时效性 开销
定时轮询(每 30 秒) 最差延迟 30 秒 大量无效请求
长轮询 秒级 连接挂起,无变更时不消耗资源
WebSocket 推送 实时 连接管理复杂

长轮询的做法是:客户端发起请求,服务端如果发现配置没变,就把这个请求挂起不返回(通常 30 秒超时);一旦配置有变更,立刻响应。这样既做到了秒级感知,又避免了高频空轮询。

五、哪些配置适合热更新

不是所有配置都能热更新,这点经常被忽略。

适合热更新的

配置类型 例子
业务参数 订单超时时间、单用户最大订单数
限流阈值 QPS 上限、熔断触发比例
功能开关 是否开启某功能、灰度比例
文案/白名单 提示文案、IP 白名单
重试策略 重试次数、退避间隔

共同特点:改完立即生效不会有副作用,且随时可回退。

不适合热更新的

配置类型 为什么不行
数据源连接串 需要重建连接池,涉及连接迁移,风险高
端口、启动参数 需要重新绑定端口
序列化/加密密钥 变更会导致新旧数据不兼容,需要版本化方案
线程池核心参数 虽然 setCorePoolSize() 能改,但要评估队列积压情况
注册中心地址 改了可能导致服务失联
日志级别 这个通常可以(有专门的动态调整接口)

判断标准:配置变更是否需要重建某种长期持有的资源(连接池、线程池、端口绑定)。需要的话,热更新就有风险,应该走重启流程。

一个隐蔽的坑:配置刷新时的并发问题

假设限流阈值从 1000 改成 2000,而代码是这样写的:

if (currentQps > limitQps) {
    throw new RateLimitException();
}

单看没问题。但如果配置变更的同时有大量请求正在执行,limitQps 的读取和判断之间可能拿到不同的值——这在大多数场景下无害(限流阈值本来就是软约束),但如果配置项之间存在关联约束(比如 min 必须小于 max),就可能在刷新瞬间读到不一致的组合。

解决方案:把相关配置放在一个 @ConfigurationProperties 类里,让它们作为一个整体替换,而不是逐个字段更新。

六、实践建议

建议 理由
配置项都给默认值 配置中心挂掉时服务仍能启动
敏感配置加密 数据库密码、密钥不要明文存放
变更要审计 谁在什么时候改了什么,出问题能追溯
重要配置灰度 先推给一个实例观察,再全量
配置分类管理 按服务、按环境分组,不要全堆在一个文件
不要什么都放配置中心 几乎不变的配置写在本地文件即可

最后一条值得强调:配置中心不是把 application.yml 搬个地方。它的价值在于管理那些需要动态调整的配置。如果把几十个永远不变的配置也搬进去,只会让配置来源变复杂,排查问题时还要多查一个地方。

小结

  • 配置中心解决两个问题:集中管理和动态刷新
  • 两种接入方式:@RefreshScope + @Value(零散配置)、@ConfigurationProperties(一组配置,推荐)
  • @RefreshScope 的原理是销毁并重建 Bean,所以只用在无状态类上
  • 动态刷新的时效性靠长轮询保证:无变更时挂起连接,有变更时立即响应——比定时轮询省资源,比 WebSocket 简单
  • 不是所有配置都能热更新:涉及重建长期资源(连接池、端口)的配置应该走重启
  • 关联配置要放在同一个 @ConfigurationProperties 类里整体替换,避免刷新瞬间读到不一致的组合
  • 配置项必须给默认值,否则配置中心故障会导致服务无法启动
  • 别把配置中心当"远程 application.yml"用——只有需要动态调整的配置才放进去

配置热更新的价值不只是省一次发布,更重要的是让调整参数这件事从"工程流程"变成"运营操作"。大促前调限流阈值不再需要协调发版,这才是它真正的意义。

留下点什么

评论基于 GitHub Discussions(giscus)。在项目根目录配置.env.local填入 NEXT_PUBLIC_GISCUS_* 四个变量后自动开启。