ZooKeeper 完整 CAS 与 Watch 阅读项目
程序合同
ZooKeeper 完整示例 在独立实验根路径 /notes-course 中读取数据、安装一次性 watch、用版本 CAS 写 v2,再创建并主动删除临时顺序节点。依赖为 ZooKeeper client 3.9.3、JDK 17、Maven 3.9;运行环境由读者自行准备。固定根路径不可与正式业务共用。示例未执行,仅有静态审阅,不声称依赖下载、Java 编译或协议互通已通过。
程序退出码 0 表示预期步骤完成,2 表示 BadVersion 冲突,1 表示其余错误。程序显式 System.exit,使 SDK 后台线程不决定 CLI 的退出时刻;资源关闭仍在 finally/try-with-resources 中进行。父节点是 persistent,重跑会读现有值;临时子节点在会话关闭后也会清理,因此观察到创建日志不等于进程退出后还会存在。
完整文件
ZooKeeper 源码配置 全部列于下方,pom.xml 固定产品依赖和编译插件,不依靠本站 node_modules。ZK_AUTH 是读者提供的 digest 用户名与密码,CREATOR_ALL_ACL 将新节点权限授予已认证创建者;它不是 TLS,跨不可信网络仍需另设传输保护。已有父节点的 ACL 不被本程序改写。
<project xmlns="http://maven.apache.org/POM/4.0.0"><modelVersion>4.0.0</modelVersion><groupId>notes</groupId><artifactId>zk-reader</artifactId><version>1.0</version><properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.9.3</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.13.0</version></plugin><plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>3.5.0</version><configuration><mainClass>notes.Coordination</mainClass></configuration></plugin></plugins></build></project>package notes;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
public class Coordination {
public static void main(String[] args) {
int code = 0;
try { run(args); }
catch (KeeperException.BadVersionException e) { System.err.println("CAS_CONFLICT"); code = 2; }
catch (Exception e) { System.err.println(e.getClass().getSimpleName() + ": " + e.getMessage()); code = 1; }
System.exit(code);
}
static void run(String[] args) throws Exception {
String address = System.getenv().getOrDefault("ZK_CONNECT", "127.0.0.1:2181");
CountDownLatch connected = new CountDownLatch(1);
try (ZooKeeper zk = new ZooKeeper(address, 10000, event -> {
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) connected.countDown();
if (event.getState() == Watcher.Event.KeeperState.Expired) System.err.println("SESSION_EXPIRED: ownership lost");
})) {
if (!connected.await(15, TimeUnit.SECONDS)) throw new IllegalStateException("CONNECT_TIMEOUT");
String auth = System.getenv("ZK_AUTH");
if (auth == null || !auth.contains(":")) throw new IllegalArgumentException("ZK_AUTH=user:password required");
zk.addAuthInfo("digest", auth.getBytes(StandardCharsets.UTF_8));
String path = "/notes-course";
try { zk.create(path, "v1".getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT); }
catch (KeeperException.NodeExistsException ignored) { /* existing node retains its ACL */ }
CountDownLatch changed = new CountDownLatch(1);
Stat before = new Stat();
byte[] value = zk.getData(path, event -> {
if (event.getType() == Watcher.Event.EventType.NodeDataChanged) changed.countDown();
else System.err.println("WATCH_STATE=" + event.getState() + " TYPE=" + event.getType() + "; re-read before resuming");
}, before);
System.out.println("READ=" + new String(value, StandardCharsets.UTF_8) + " VERSION=" + before.getVersion());
int expected = args.length == 0 ? before.getVersion() : Integer.parseInt(args[0]);
zk.setData(path, "v2".getBytes(StandardCharsets.UTF_8), expected);
if (!changed.await(5, TimeUnit.SECONDS)) throw new IllegalStateException("WATCH_TIMEOUT");
// Re-read AND install the next one-shot watch. This is not an event log.
Stat after = new Stat();
zk.getData(path, event -> System.out.println("NEXT_EVENT=" + event.getType()), after);
String member = zk.create(path + "/member-", new byte[0], ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("UPDATED_VERSION=" + after.getVersion() + " EPHEMERAL=" + member);
System.out.println("Not a distributed lock: no protected-resource fencing is implemented.");
zk.delete(member, -1);
}
}
}自跑命令与预期
ZooKeeper 示例运行 命令仅供读者在自己的隔离环境执行,本轮不执行 Maven,也不下载客户端。POSIX shell 中先设 ZK_CONNECT 与 ZK_AUTH;PowerShell 用 $env:ZK_CONNECT 和 $env:ZK_AUTH。
cd examples/zookeeper
export ZK_CONNECT=127.0.0.1:2181
export ZK_AUTH='reader:replace-in-your-lab'
mvn compile exec:java
mvn compile exec:java -Dexec.args=2147483647正常预期:READ 显示旧数据与 VERSION,UPDATED_VERSION 增加一,EPHEMERAL 输出实际路径,退出 0。第二条程序命令用几乎不可能匹配的预期版本演示 CAS_CONFLICT,预期退出 2;Maven 进程本身可能把子程序非零包装为插件失败状态,判断冲突应结合输出,不能保证 Maven 外层原样透传 2。要观察本程序原始退出码,可用自己已准备的完整运行类路径直接执行 java -cp "$ZK_CP" notes.Coordination 2147483647。ZK_CP 必须包括 target/classes 及依赖 JAR,不能只有单个 zookeeper JAR。
连接地址不可达预期 CONNECT_TIMEOUT 并退出 1;已有父节点但身份错误预期 NoAuth 并退出 1。不要为让它通过而删除别人的节点。示例保留持久父节点供下次版本练习,读者完成后用有权身份检查并删除 /notes-course,先确认其子节点为空且属于本实验。
阶段实践与解答
ZooKeeper 阶段实践 先在纸上安排 A、B 都读版本 0,再让 A 写 1、B 用 0 写。解答:B 的 BadVersion 是保护数据的成功,不是应绕开的障碍。然后推演两次写发生在一次 watch 触发与重注册之间:程序只能重新读最新快照,不能承诺收到两个回调。最后指出本例为何不是锁:它没选举最小候选者、没监听前驱、没给外部资源安装 fencing 校验。
核对 2026-10-17,依据 3.9.3 Programmer's Guide。示例未执行,尤其未验证竞态与线程时序。
继续阅读
- 知识专题
- 问题排查