ZooKeeper 分布式锁

某团队在使用 ZooKeeper 实现分布式锁时,发现锁的获取速度比 Redis 慢 10 倍。

排查后发现:每次获取锁都要在 ZooKeeper 中创建临时顺序节点,而 ZooKeeper 的写操作需要 Leader 节点处理,高并发下成为瓶颈。

但 ZooKeeper 分布式锁有一个 Redis 分布式锁无法替代的优势:锁的可靠性更高,不会出现 Redis 主从切换导致的锁丢失问题。

【架构权衡】 ZooKeeper 分布式锁和 Redis 分布式锁是两种不同的实现思路。Redis 锁性能高但存在主从切换风险,ZooKeeper 锁可靠性高但性能较低。选择哪种,取决于业务对"一致性"和"性能"的权衡。


一、核心问题 🔴

1.1 ZooKeeper 数据模型

ZooKeeper 的数据结构:

ZooKeeper 将数据存储为树形结构(类似于文件系统):

/
├── /locks
│   ├── /locks/lock-000000001  ← 临时顺序节点
│   ├── /locks/lock-000000002  ← 临时顺序节点
│   └── /locks/lock-000000003  ← 临时顺序节点
└── /services
    └── /services/order        ← 持久节点

1.2 ZooKeeper 分布式锁原理

┌─────────────────────────────────────────────────────────────────┐
│                    ZooKeeper 分布式锁原理                          │
│                                                                  │
│  获取锁:                                                          │
│  1. 在 /locks 下创建临时顺序节点                                  │
│     └─ /locks/lock-000000001                                    │
│                                                                  │
│  2. 获取 /locks 下所有子节点                                      │
│     └─ [lock-000000001, lock-000000002, lock-000000003]         │
│                                                                  │
│  3. 判断自己是否是序号最小的节点                                 │
│     └─ 如果是,获取锁成功                                        │
│     └─ 如果不是,Watcher 监听上一个节点                         │
│                                                                  │
│  释放锁:                                                          │
│  └─ 删除自己的临时节点                                            │
│     └─ 删除时触发下一个节点的 Watch                               │
│     └─ 下一个节点收到通知,再次判断是否最小                      │
└─────────────────────────────────────────────────────────────────┘

二、方案实现

2.1 Curator 的 InterProcessMutex

// 使用 Curator 实现分布式锁
@Service
public class ZkDistributedLock {

    @Autowired
    private CuratorFramework curatorFramework;

    public void executeWithLock(String lockPath, Runnable task) {
        // 创建分布式锁
        InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);

        try {
            // 尝试获取锁,等待 30 秒
            boolean acquired = lock.acquire(30, TimeUnit.SECONDS);

            if (acquired) {
                task.run();
            } else {
                throw new RuntimeException("获取 ZooKeeper 分布式锁失败");
            }
        } catch (Exception e) {
            throw new RuntimeException("获取 ZooKeeper 分布式锁异常", e);
        } finally {
            try {
                // 释放锁
                if (lock.isOwnedByCurrentThread()) {
                    lock.release();
                }
            } catch (Exception e) {
                log.error("释放 ZooKeeper 分布式锁失败", e);
            }
        }
    }
}

2.2 手写分布式锁(理解原理)

// 手写 ZooKeeper 分布式锁
@Service
public class SimpleZkLock {

    @Autowired
    private CuratorFramework curatorFramework;

    private static final String LOCK_PATH = "/distributed-locks";

    public String acquireLock(String lockKey, long timeoutMs) throws Exception {
        // 创建父节点(如果不存在)
        if (curatorFramework.checkExists().forPath(LOCK_PATH) == null) {
            curatorFramework.create().withMode(CreateMode.PERSISTENT).forPath(LOCK_PATH);
        }

        // 创建临时顺序节点
        String nodePath = curatorFramework.create()
            .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
            .forPath(LOCK_PATH + "/" + lockKey + "-", new byte[0]);

        // 获取锁
        long startTime = System.currentTimeMillis();
        while (System.currentTimeMillis() - startTime < timeoutMs) {
            // 获取所有子节点
            List<String> children = curatorFramework.getChildren()
                .forPath(LOCK_PATH);

            // 按序号排序
            Collections.sort(children);
            String smallest = children.get(0);

            // 如果自己是最小节点,获取锁成功
            if (nodePath.endsWith(smallest)) {
                return nodePath;
            }

            // 监听上一个节点
            String previousNode = findPreviousNode(children, nodePath);
            CountDownLatch latch = new CountDownLatch(1);

            curatorFramework.getData()
                .usingWatcher(new Watcher() {
                    @Override
                    public void process(WatchedEvent event) {
                        if (event.getType() == Event.EventType.NodeDeleted) {
                            latch.countDown();
                        }
                    }
                })
                .forPath(LOCK_PATH + "/" + previousNode);

            // 等待上一个节点删除
            latch.await(timeoutMs, TimeUnit.MILLISECONDS);
        }

        // 超时,删除自己创建的节点
        curatorFramework.delete().forPath(nodePath);
        return null;
    }

    public void releaseLock(String nodePath) throws Exception {
        curatorFramework.delete().forPath(nodePath);
    }
}

【架构权衡】 ZooKeeper 分布式锁的核心优势是可靠性:临时节点和 Watch 机制保证了锁的正确性,即使 ZooKeeper 节点崩溃也会自动释放锁。但它的性能不如 Redis,因为每次锁操作都需要 ZooKeeper 服务端的参与。

三、Redis vs ZooKeeper

对比维度Redis 分布式锁ZooKeeper 分布式锁
实现复杂度
性能高(内存操作)低(网络 + 服务端处理)
可靠性中(主从切换可能丢锁)高(ZAB 协议保证一致性)
可重入需要额外实现Curator 原生支持
公平性非公平公平(按序号)
主从切换有风险无风险
适用场景高并发高可靠性

四、落地 Checklist

  • 客户端:使用 Curator 客户端库
  • 连接管理:连接池配置、Session 超时
  • 超时设置:合理设置获取锁和等待时间
  • 释放保证:finally 中释放锁
  • 监控告警:监控锁获取失败率