基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

如何使用 ZooKeeper 实现分布式锁?

知识点图片

使用 ZooKeeper 实现分布式锁是一个非常经典和可靠的应用场景。下面我将从核心原理具体实现步骤代码示例以及优缺点分析四个方面来详细讲解。

一、核心原理:利用临时顺序节点

ZooKeeper 实现分布式锁的核心是利用其 临时顺序节点 的特性。

  1. 临时节点:客户端与 ZooKeeper 的会话断开后,该节点会自动被删除。这可以防止客户端崩溃后锁无法释放(死锁)。
  2. 顺序节点:在创建节点时,ZooKeeper 会自动在路径后面追加一个单调递增的序号(如 /lock/lock-0000000001, /lock/lock-0000000002)。

基于以上两点,我们可以设计如下的锁获取逻辑:

  • 锁目录:在 ZooKeeper 中创建一个持久节点作为锁的根目录,例如 /locks/my_lock
  • 获取锁:所有客户端尝试在 /locks/my_lock 下创建一个临时顺序节点
  • 判断序号:客户端创建节点后,获取 /locks/my_lock 下的所有子节点,并判断自己创建的节点是否是序号最小的那个。
    • 如果是最小的:则成功获取锁,执行业务逻辑。
    • 如果不是最小的:说明有其他客户端持有锁。此时,当前客户端需要监听它前一个(序号比它小的最大节点)节点的删除事件。
  • 等待与唤醒:如果前一个节点被删除了(意味着持有锁的客户端释放了锁或会话超时),ZooKeeper 会通知监听它的客户端。客户端收到通知后,重新执行“判断序号”的逻辑,检查自己是否成为了序号最小的节点。
  • 释放锁:业务逻辑执行完毕后,主动删除自己创建的临时顺序节点。或者,如果客户端崩溃,会话超时,ZooKeeper 也会自动删除该节点。

这种机制被称为 “羊群效应”优化“公平锁” 的实现,因为每个客户端只监听它前面的一个节点,避免了大量客户端同时监听同一个节点而导致的惊群问题。


二、具体实现步骤(Curator 框架)

虽然可以原生 ZooKeeper API 实现,但过程较为繁琐。实际生产环境中,强烈推荐使用 Apache Curator 这个开源客户端,它提供了高层次的分布式锁封装。

以下是使用 Curator 实现分布式锁的步骤:

1. 添加 Maven 依赖

xml
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.5.0</version> <!-- 请使用最新版本 -->
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.8.0</version> <!-- 版本需与你的ZK服务器兼容 -->
</dependency>

2. 编写代码

java
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

public class ZkDistributedLockExample {

    // ZooKeeper 连接字符串
    private static final String ZK_ADDRESS = "localhost:2181";
    // 锁的根路径
    private static final String LOCK_PATH = "/my_app/locks/my_distributed_lock";

    public static void main(String[] args) {
        // 1. 创建 Curator 客户端
        CuratorFramework client = CuratorFrameworkFactory.newClient(
                ZK_ADDRESS,
                new ExponentialBackoffRetry(1000, 3) // 重试策略:初始休眠1s,最多重试3次
        );
        client.start();

        // 2. 创建分布式可重入互斥锁
        InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH);

        try {
            // 3. 尝试获取锁
            if (lock.acquire(10, TimeUnit.SECONDS)) { // 最多等待10秒获取锁
                try {
                    // 4. 成功获取锁,执行业务逻辑
                    System.out.println(Thread.currentThread().getName() + " 成功获取锁,开始工作...");
                    // 模拟业务操作耗时
                    Thread.sleep(5000);
                    System.out.println(Thread.currentThread().getName() + " 工作完成,释放锁。");
                } finally {
                    // 5. 务必在 finally 块中释放锁!
                    lock.release();
                    System.out.println(Thread.currentThread().getName() + " 锁已释放。");
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " 获取锁失败!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭客户端
            client.close();
        }
    }
}

关键点解释:

  • InterProcessMutex:Curator 提供的分布式可重入互斥锁,就是我们上面描述的基于临时顺序节点实现的公平锁。
  • acquire(timeout, unit):尝试获取锁。可以设置超时时间,避免无限期等待。
  • release()必须在 finally 块中调用,以确保锁一定会被释放,防止死锁。
  • 可重入性:同一个线程可以多次 acquire 而不会阻塞自己,但需要对应次数的 release

三、原生 ZooKeeper API 简要思路(了解即可)

如果不使用 Curator,使用原生 API,你需要手动处理以下步骤:

  1. 创建客户端ZooKeeper zk = new ZooKeeper(...);
  2. 创建锁节点zk.create("/locks/my_lock/lock-", ... , CreateMode.EPHEMERAL_SEQUENTIAL),注意模式是 EPHEMERAL_SEQUENTIAL
  3. 获取所有子节点并排序List<String> children = zk.getChildren("/locks/my_lock", false);,然后对列表按序号排序。
  4. 判断自己是否是最小节点
    • 如果是,加锁成功。
    • 如果不是,找到前一个节点(序号最大的且小于自己的节点)。
  5. 注册监听:对前一个节点注册 Watcher,监听 NodeDeleted 事件。
  6. 等待:在 Watcher 的 process 方法中被唤醒后,回到第 3 步重新检查。
  7. 释放锁zk.delete(自己创建的节点路径, -1);

这个过程非常复杂且容易出错,因此强烈不推荐在生产环境使用原生 API 实现。


四、优缺点分析

优点:

  1. 高可靠性:基于 ZooKeeper 的集群特性,只要大多数节点存活,服务就可用。临时节点避免了死锁。
  2. 公平锁:通过顺序节点实现了先到先得的公平性,避免了“饥饿”现象。
  3. 高性能:Curator 等客户端做了大量优化,性能表现良好。
  4. 可重入性:Curator 的 InterProcessMutex 支持可重入。

缺点:

  1. 复杂性:部署和维护 ZooKeeper 集群本身有一定成本。
  2. 性能开销:每次获取锁都需要在 ZooKeeper 中创建和删除节点,网络交互较多,性能通常低于基于 Redis 的 RedLock 算法(但在一致性要求极高的场景下,ZK 更可靠)。
  3. 会话超时问题:如果网络抖动导致客户端会话超时,锁会被意外释放,可能导致临界区代码执行到一半时被打断。需要业务层有幂等性或补偿机制来处理这种情况。

总结

使用 Apache CuratorInterProcessMutex 是在 ZooKeeper 上实现分布式锁的最佳实践。它隐藏了底层复杂的节点操作和 Watcher 管理,提供了简洁、健壮且功能丰富的 API。在选择分布式锁方案时,如果对一致性和可靠性要求极高,ZooKeeper 是一个绝佳选择;如果追求极致性能且能接受一定的数据不一致风险,可以考虑基于 Redis 的方案。

00:00
00:00