使用临时顺序节点而非普通临时节点,因其自增序号可解决“谁先谁后”竞争问题;普通临时节点同名创建会抛NodeExistsException。

为什么用临时顺序节点而不是普通临时节点
因为普通临时节点没法解决“谁先谁后”的竞争问题。多个客户端同时创建同名临时节点会直接报 NodeExistsException
临时顺序节点由 ZooKeeper 自动在路径末尾追加单调递增的序号(如 _0000000001),天然形成排队队列。
实操时注意:
- 节点路径必须以 / 开头,且父节点需提前存在(ZooKeeper 不支持自动创建多级路径)
- 创建节点必须指定 CreateMode.EPHEMERAL_SEQUENTIAL,写成 EPHEMERAL 就失去顺序性
- 序号是 10 位数字,不是时间戳,也不保证全局连续(只保证本父节点下递增)
Watcher 只触发一次,怎么持续监听前驱节点
ZooKeeper 的 exists() 或 getData() 注册的 Watcher 在事件触发后就自动失效,这是最常踩的坑——锁释放后没人再监听,后续客户端永远卡住。
正确做法是:每次监听被触发后,立刻重新调用 exists() 注册新 Watcher。
立即学习“Java免费学习笔记(深入)”;
常见错误场景:
- 在 process(WatchedEvent) 回调里只做日志,没重注册
- 使用 getChildren() 监听子节点变化,但没对每个前驱节点单独注册 Watcher(它只能监听子节点列表增减,不能知道某个具体节点是否删除)
- 忘记判断事件类型,把 NodeDeleted 和 NodeCreated 混为一谈
如何安全判断自己是不是下一个获得锁的客户端
核心逻辑是:获取父节点下所有子节点列表 → 按序号排序 → 找出自己的节点 → 看它前面有没有更小序号的存活节点。
关键细节:
- 排序必须用字符串自然排序(ZooKeeper 序号固定 10 位,所以 "_0000000009" "_0000000010"),别用数值解析,否则会溢出或截断
- 判断“前驱是否存在”不能只查名字,得调用 exists() 确认该节点当前是否还活着(防止刚删完、列表还没刷新)
- 如果前驱节点不存在,说明轮到你了;存在,则对它注册 exists() Watcher,等它消失
示例片段:
String myPath = zk.create("/lock/req-", null, Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);<br>String mySeq = myPath.substring(myPath.lastIndexOf("-") + 1);<br>List<String> children = zk.getChildren("/lock", false);<br>Collections.sort(children); // 字符串排序即可<br>int i = children.indexOf("req-" + mySeq);<br>if (i == 0) { /* 拿到锁 */ } else { zk.exists("/lock/" + children.get(i-1), watcher); }
连接断开时锁自动失效,但业务可能还在执行
临时节点确实会在 session 过期或连接断开时被 ZooKeeper 自动删除,这是优势,但也是隐患:网络抖动导致连接短暂中断,节点被删,而你的业务线程还在处理,结果出现“锁丢了但活没干完”。
应对方式:
- 设置合理的 sessionTimeout(通常 15–30 秒),太短易误删,太长导致故障恢复慢
- 业务代码中必须有超时控制,绝不能依赖锁长期持有
- 更稳妥的做法是引入租约机制:加锁时写入一个带时间戳的临时节点内容,定期用 setData() 续期,业务结束前校验时间戳是否过期
另外,ZooKeeper 客户端默认不自动重连,CuratorFramework 会帮你兜底,原生 ZooKeeper 类需要自己监听 SyncConnected 事件并重建锁逻辑。



















