3.3 Curator 实战:流式接口、重试与缓存监听 本节摘要:Curator 用流式接口重构了增删改查,用重试策略治理了瞬时故障,用 NodeCache 与 TreeCache 把一次性 Watcher 变成持续订阅。本节覆盖框架级用法,为第五章所有判例提供公共代码底座。 一条 fromPath 开始的链 Curator 的日常用法可以浓缩成三段式:构建客户端、流式操作、缓存监听。先看客户端构建——它把 3.2 节那些"等会话建立、处理重连、区分异常"的脏活全部收编进框架: 两个参数值得多说一句。namespace 让不同应用在同一集群里互不可见,是多人共用一套 ZooKeeper 时最值得开的开关;
本节摘要:Curator 用流式接口重构了增删改查,用重试策略治理了瞬时故障,用 NodeCache 与 TreeCache 把一次性 Watcher 变成持续订阅。本节覆盖框架级用法,为第五章所有判例提供公共代码底座。
Curator 的日常用法可以浓缩成三段式:构建客户端、流式操作、缓存监听。先看客户端构建——它把 3.2 节那些"等会话建立、处理重连、区分异常"的脏活全部收编进框架:
// 声明式构建:连接串 + 会话与连接超时 + 命名空间 + 重试策略 CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("127.0.0.1:2181") // 多台逗号分隔,如 node1:2181,node2:2181 .sessionTimeoutMs(30000) // 会话超时,服务端会按边界夹取 .connectionTimeoutMs(15000) // TCP 建连超时,与会话超时是两回事 .namespace("myapp") // 所有操作自动加 /myapp 前缀,多应用隔离 .retryPolicy(new ExponentialBackoffRetry(1000, 3)) // 指数退避:1s 起,最多 3 次 .build(); client.start(); client.blockUntilConnected(); // 阻塞到会话可用,等价 3.2 的 countDown 写法
两个参数值得多说一句。namespace 让不同应用在同一集群里互不可见,是多人共用一套 ZooKeeper 时最值得开的开关;重试策略的语义要弄准——它只重试可重试异常(连接类),不会把"节点已存在"这类业务性失败也重试掉,所以不必担心重试放大副作用。
同一个动作,原生 API 五个参数排一排,Curator 则是从动作出发一路点到底,读起来接近自然语言:
// 增:父节点不存在则连带创建,等价 mkdir -p client.create().creatingParentsIfNeeded() .forPath("/config/db/url", "jdbc:demo".getBytes()); // 改:带版本的条件更新,版本不符抛 BadVersion,等价乐观锁 Stat stat = new Stat(); client.getData().storingStatIn(stat).forPath("/config/db/url"); client.setData().withVersion(stat.getVersion()) .forPath("/config/db/url", "jdbc:v2".getBytes()); // 查:一个调用同时拿数据与 stat byte[] data = client.getData().storingStatIn(new Stat()).forPath("/config/db/url"); // 删:guaranteed 保证在网络抖动下删除动作最终完成(后台重试到成功为止) client.delete().guaranteed().deletingChildrenIfNeeded().forPath("/config");
guaranteed() 的语义容易被误解成"事务保证",其实它承诺的是删除的最终完成:普通删除遇到会话重连可能中途失败,guaranteed 模式由框架在后台持续重试,适合"删除必须成功"的锁清理场景。第五章的锁实现里你会看到它反复出现。
3.2 节手写的"读-听循环"在 Curator 里有两个现成形态。NodeCache 盯单个节点的数据变化,TreeCache 盯一棵子树。它们不仅自动重挂监听,还在本地维护一份当前值副本——监听回调里直接取,不必再发一次读请求:
// NodeCache:单节点持续监听 NodeCache nodeCache = new NodeCache(client, "/config/db/url"); nodeCache.getListenable().addListener(() -> { ChildData cd = nodeCache.getCurrentData(); if (cd != null) { System.out.println("配置更新: " + new String(cd.getData())); // 这里触发应用内配置刷新,注意别做重活 } else { System.out.println("节点被删除了"); // 删除也是事件,分支别漏 } }); nodeCache.start(true); // true 表示启动时立即拉一次当前值 // TreeCache:整棵子树监听,服务注册场景的主力 TreeCache treeCache = new TreeCache(client, "/services/pay"); treeCache.getListenable().addListener((c, event) -> { switch (event.getType()) { case NODE_ADDED: System.out.println("实例上线: " + event.getData().getPath()); break; case NODE_REMOVED: System.out.println("实例下线: " + event.getData().getPath()); break; case NODE_UPDATED: System.out.println("实例信息变更: " + event.getData().getPath()); break; default: break; } }); treeCache.start();
用 TreeCache 做服务发现的完整判例在 5.4 节。这里只需要记住分界线:盯一个值用 NodeCache,盯一份成员名单用 TreeCache,其余场景先想想是否真的需要监听。
长驻服务里客户端随进程生死,一次性任务里则要显式收尾。忘 close 的会话会拖到超时才被服务端清理,期间临时节点一直占着位——用临时节点做选主的服务会顶着"僵尸主节点"运行到超时为止,这是联调期最常见的灵异现象:
// 缓存先关、客户端后关,顺序别反 nodeCache.close(); treeCache.close(); client.close();
# 服务端侧验证会话清理:命令行列出当前会话 $ echo cons | nc 127.0.0.1 2181 /127.0.0.1:52345[1](queued=0,recved=42,sent=42,sid=0x1000a3f2c1,LAST=...) # sid 就是会话 id;任务结束后再看,对应条目消失即为清理干净
问:NodeCache 监听期间节点被删又重建,还能收到通知吗? 能。NodeCache 把删除与重建都当事件派发,getCurrentData 返回 null 代表删除态。但若你的业务假设"节点永远在",就该在 null 分支里做兜底重建。
工具箱备齐,第四章进入全册核心:法庭内部到底如何运转。