配置热更新:改配置不重启服务
把配置写死在代码或打包进镜像是常见的做法,但改一个限流阈值就要重新发布,成本太高。这篇文章讲清配置中心的两种接入方式、动态刷新的原理,以及哪些配置适合热更新。
一、一个很常见的低效场景
线上的限流阈值设成了 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_* 四个变量后自动开启。