5.1 判例一:配置中心的动态下发 本节摘要:用持久节点存配置、NodeCache 监听变化、回调刷新应用内配置对象,三十行代码搭出一个带动态下发能力的配置中心。本判例重点在回调设计与并发更新两个细节,它们决定了这套东西能不能进生产。 背景:改个线程池大小为什么要重启 某推送服务为了调"批量发送大小"这类参数,要滚动重启一百台机器,每次调整耗时四十分钟。参数本质上是"所有机器都该一致知道的一份数据",而 ZooKeeper 的持久节点加监听组合恰好是为它准备的:数据在案(强一致),变化有传票(Watcher)。判例目标:参数变更后五秒内全场生效,全程无重启。 实现:一份配置的全场同步 先设计节点布局——路径即语义,这比代码更重要: 应用侧是本判例的主体:启动时拉全量、挂监听、回调刷新。
本节摘要:用持久节点存配置、NodeCache 监听变化、回调刷新应用内配置对象,三十行代码搭出一个带动态下发能力的配置中心。本判例重点在回调设计与并发更新两个细节,它们决定了这套东西能不能进生产。
某推送服务为了调"批量发送大小"这类参数,要滚动重启一百台机器,每次调整耗时四十分钟。参数本质上是"所有机器都该一致知道的一份数据",而 ZooKeeper 的持久节点加监听组合恰好是为它准备的:数据在案(强一致),变化有传票(Watcher)。判例目标:参数变更后五秒内全场生效,全程无重启。
先设计节点布局——路径即语义,这比代码更重要:
# 运维在集群上建立的配置树(一次性动作) [zk] create /myapp/config "base" # namespace 根,4.5 节的 ACL 建议在这里设 [zk] create /myapp/config/push-batch "500" # 批量发送大小 [zk] create /myapp/config/pay-timeout "3000" # 支付超时毫秒 [zk] create /myapp/config/switch-gray "off" # 灰度开关
应用侧是本判例的主体:启动时拉全量、挂监听、回调刷新。Curator 写法:
public class ZkConfigCenter { private final CuratorFramework client; private final Map<String, String> cache = new ConcurrentHashMap<>(); private final Map<String, NodeCache> watchers = new ConcurrentHashMap<>(); public ZkConfigCenter(CuratorFramework client) { this.client = client; } public void watch(String key, Consumer<String> onChange) throws Exception { String path = "/myapp/config/" + key; // 启动即拉全量:监听只保证"变化可知",初始值要自己取 byte[] data = client.getData().forPath(path); cache.put(key, new String(data)); NodeCache nodeCache = new NodeCache(client, path); nodeCache.getListenable().addListener(() -> { ChildData cd = nodeCache.getCurrentData(); String newVal = cd == null ? null : new String(cd.getData()); String oldVal = cache.put(key, newVal); // 原子更新本地缓存 System.out.println("配置 " + key + ": " + oldVal + " 变为 " + newVal); if (onChange != null && newVal != null) { onChange.accept(newVal); // 回调只做轻活:更新内存对象 } }); nodeCache.start(true); watchers.put(key, nodeCache); } } // 使用:把配置直接映射成应用内的行为参数 configCenter.watch("push-batch", v -> sender.setBatchSize(Integer.parseInt(v))); // 热生效,无重启 configCenter.watch("switch-gray", v -> graySwitch.setOn("on".equals(v)));

上线验证三步:改值、看日志、查行为。用 zkCli 改 push-batch 从 500 到 200,各实例日志应在秒级打出"配置 push-batch: 500 变为 200",随后业务行为按新值运行。四个字命令或应用指标都能确认——配置生效时间远短于任何一次滚动重启。
两个人同时改同一个配置,后写的覆盖先写的,谁都不知道。解法是 1.2 节的版本号条件更新,管理后台的保存动作应带版本校验:
// 读出当前值与版本,界面上回显;保存时带版本提交 Stat stat = new Stat(); String current = new String(client.getData().storingStatIn(stat).forPath(path)); try { client.setData().withVersion(stat.getVersion()).forPath(path, newVal.getBytes()); } catch (BadVersionException e) { throw new IllegalStateException("配置已被他人修改,请刷新后重试"); }
ZNode 没有历史版本,回滚要么靠"上一次的值还留在管理后台的数据库里",要么在节点里多存一层 JSON(value 加 version 加 updatedBy)。判断标准很简单:配置项少而敏感,值得多存元信息;量大就交给专业配置系统,ZooKeeper 只做它擅长的"变更通知"这一段。反过来提醒一句边界:别把 ZooKeeper 当数据库——它没有历史、没有查询语言、单节点 1 MB 上限,配置量大、变更频繁、需要权限审批流时,专业配置中心更合适。
问:实例断线期间错过多次变更怎么办? NodeCache 在重连后会全量拉取当前值,最终状态正确;若业务需要感知每一次中间变化,那不是配置中心该承担的,那是消息系统的活。
下一个判例含金量最高:几十个进程抢同一把锁,怎么排号、怎么叫号、怎么保证持锁者死了锁不悬空。