事后复盘,这事的根子就一句话: @RefreshScope 被我贴错了类。
值本身住在 AsyncConfig, 那枚注解却跟到了 Service 身上。
scope 从没罩住存值的这只对象, 它自打开机起就一直没换过人。
要讲明白,还得退回到当初。
那阵子要挪线程池的核心数, 定的是把 8 换成 16。
改动在 Nacos 那头落下去,按了发布。
不过两秒,应用日志甩过来一句:
Refresh keys changed: [thread.pool.core-size]
瞄到这行,我以为已经到位。
谁料程序里 echo 出来的数, 屏幕上摆着的仍旧是 8。
直到重启了一回, 它才不情不愿认了新值。
其实那行日志能证明的范围很窄, 仅够表示 Environment 里的键动过了。
我这只 bean 的字段 有没有被回填第二遍, 它压根不替谁背书。
我之前写过〈实体类字段用 @Value 赋了默认值,取出来还是 null?〉, 那一篇交代过装配次序: 容器先把对象 new 出来, 再倒回去往字段里灌。
这一桩算它的续集: 灌只灌了头一遭, 往后谁也没再动第二手。
java
`@Component
public class AsyncConfig {
@Value("${thread.pool.core-size}")
private int coreSize;
}
`
字段要跟着换新, 门槛只有一个:这只 bean 得归 refresh scope 管。
说白了,类头顶一枚 @RefreshScope。
偏偏我把它安错了门牌。
java
`public PoolHolder(AsyncConfig cfg) {
this.core = cfg.getCoreSize(); // 从这一刻起它与配置再无瓜葛
}
`
还有一种更刁钻的:
scope 挂得没毛病,新对象也揣上了新值。
坏就坏在 Service 刚 new 的时候, 就把 coreSize 抄进了自家字段, 等于私下留了份快照。
配置那头换了血, 这头还死死攥着那个旧的 8。
@Value 这么一张张散着贴, 能翻的车就这两起。
至于改法,收口只有一条: 一听到刷新, 就亲手把新值塞回池子。
java
`@EventListener(RefreshEvent.class)
public void onRefresh(RefreshEvent e) {
pool.setCorePoolSize(cfg.getCoreSize());
}
`
@ConfigurationProperties 那只 bean 反倒省心, 配置一变它就整个重绑一轮。
随你什么时候去捞它, 捞上来的都是新的。
我们原先摊在外面的那几项, 末了都并进了它名下。
那两下改池子大小的 set, 我不肯空口信, 搬回本机 corretto 1.8.0_412, 把 8 到 20 来回戳了几个回合:
BEFORE core=8 max=16
AFTER core=16 max=16
CORE20=ok max=16
MAX12=java.lang.IllegalArgumentException msg=null
core 抬到 20、max 还压在 16 那一下, setCorePoolSize 连吭都没吭,照单全收。
反倒是等你回手去调 setMaximumPoolSize(12), IllegalArgumentException 才姗姗抛出来。
它吐的 message 是 null, 半个字都没肯多交代。
我一直以为这俩 set 之间会互相拦一手, 趟完这趟才晓得它们各扫门前雪。
如今监听逻辑里我多摆了一道前置: 只要新 core 高过眼下的 max, 先把 max 抬起来, 再轮到 core。
临到要发布的那晚, 我还多事地把队列由 500 一口气放到 50000, 美滋滋盘算任务这下丢不着了。
结果凌晨两点四十, 告警兜头砸的就是这口池子。