bigywater.tech
ZooKeeper · 96

Curator DistributedAtomicLong

用 ZooKeeper 做全局计数器 / 发号器是常见需求。Curator 的 DistributedAtomicLong 用乐观重试实现,但「失败了不抛异常」这个设计很容易踩坑。这篇量正确性、重试耗尽时的行为、PromotedToLock、吞吐随线程数的变化,并与顺序节点发号比较。

672 / 1000
重试只允许 1 次时 succeeded()=false 的次数(不抛异常)
238 → 131
1 线程到 10 线程的吞吐(次/s),冲突让总吞吐下降
1142 个/s
顺序节点发号的吞吐(1000 个序号全部唯一)

乐观方式

实测 · 本机执行输出
  succeeded()=true 1000 次,false 0 次;最终值 1000;用时 6.6 s

原理就是乐观锁(带 version 的写):读到 version,写回时版本不符就重试。

重试耗尽

实测 · 本机执行输出
  succeeded()=true 328 次,false 672 次;最终值 328(= 成功次数,失败的 increment 没有被计入,也没有抛异常);用时 2.6 s

PromotedToLock

实测 · 本机执行输出
  succeeded()=true 1000 次,false 0 次;最终值 1000;用时 6.0 s

锁的机制见上一篇。

吞吐

实测 · 本机执行输出
   1 线程:238 次/s(成功 100 / 100)
   2 线程:206 次/s(成功 200 / 200)
   5 线程:194 次/s(成功 500 / 500)
  10 线程:131 次/s(成功 1000 / 1000)

每次 increment 至少一次读和一次写(都要经过 leader 写提交),所以它是一个低频计数器。需要每秒数万次的计数,应当在应用内批量累加后定期写入,或者用别的存储。

顺序节点发号

实测 · 本机执行输出
  1000 个序号中不同的 1000 个;用时 0.9 s(1142 个/s,含每个一次删除);最大序号 /ids/n-0000000999

顺序号的行为见顺序节点:由服务端串行分配,不会冲突。

分步演示

原子计数器
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / ZooKeeper 3.9.6(brew 安装的 jar,用 java -cp 直接启动自己的实例,OpenJDK 17 Temurin,JVM 堆 128 MiB)/ 本机回环端口 7300+,数据在临时目录;默认 tickTime=2000;计时用 time.perf_counter,数字每次运行会有小幅抖动。Curator 5.7.1 + OpenJDK 17;10 线程各用独立客户端;同机本地回环;吞吐受 fsync 影响(见第 55 篇)
    • 没有验证:SharedCount / DistributedIdQueue 等其它计数配方、PromotedToLock 的 lockPath 与重试策略的组合、跨机房的冲突率、与 Redis INCR 的对比
    京ICP备2026067502号-1京公网安备11010502063487号