Skip to content

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 不被本程序改写。

xml
<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>
java
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。

sh
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。示例未执行,尤其未验证竞态与线程时序。

继续阅读