Java 同步系列之 ZooKeeper 分布式锁

1. 引言

在分布式系统中,多个服务节点往往需要访问共享资源,例如数据库记录、缓存数据或定时任务。如果缺乏有效的互斥机制,多个节点同时操作同一份数据,很容易引发数据不一致、重复执行等问题。分布式锁正是为了解决这类跨进程、跨节点的资源竞争问题而诞生的。

ZooKeeper 作为一款成熟的分布式协调服务,天然具备创建临时顺序节点、监听节点变化等能力,非常适合用来实现分布式锁。本文将从 ZooKeeper 的数据模型讲起,逐步分析基于临时顺序节点实现分布式锁的原理,并给出完整的 Java 代码示例。

2. ZooKeeper 基础回顾

在深入分布式锁之前,先简单回顾 ZooKeeper 的几个核心概念,这些概念是理解锁实现的基础。

2.1 数据模型

ZooKeeper 的命名空间类似于一个文件系统,每个节点称为 ZNode。ZNode 既可以存储少量数据,也可以拥有子节点。常见的节点类型包括:

  • 持久节点:创建后一直存在,除非显式删除。
  • 临时节点:会话结束或超时后自动删除。
  • 顺序节点:创建时自动追加一个单调递增的序号。

分布式锁主要利用临时节点和顺序节点的组合特性。

2.2 Watcher 机制

Watcher 是 ZooKeeper 提供的事件监听机制。客户端可以针对某个节点注册监听,当该节点发生变化(如子节点新增、删除、数据变更)时,ZooKeeper 会向客户端推送通知。这一机制是锁等待与唤醒的关键。

3. 分布式锁的实现原理

基于 ZooKeeper 实现分布式锁,最经典的方式是使用临时顺序节点,整体思路可以概括为以下几步:

  1. 客户端在锁目录下创建一个临时顺序节点,例如 /locks/lock-0000000001。
  2. 客户端获取锁目录下的所有子节点,并按照序号排序。
  3. 如果自己创建的节点序号最小,说明获取锁成功。
  4. 如果自己不是最小序号,则监听前一个节点的删除事件,并进入等待状态。
  5. 当前一个节点被删除(即前一个客户端释放锁或会话超时)时,客户端被唤醒,再次检查自己是否成为最小序号。

这种方案被称为「顺序临时节点 + 前驱监听」模式,它避免了惊群效应:每个客户端只监听自己前一个节点,而不是监听所有节点。

下面用一张流程图来直观展示加锁与解锁的完整过程:

flowchart TD A[客户端创建临时顺序节点] --> B[获取锁目录全部子节点] B --> C{自己是否序号最小} C -->|是| D[获取锁成功 执行业务逻辑] C -->|否| E[监听前一个节点删除事件] E --> F[前驱节点被删除 收到通知] F --> B D --> G[删除自己的节点 释放锁]

4. Java 代码实现

下面使用 Apache Curator 框架来实现分布式锁。Curator 对 ZooKeeper 的底层 API 做了很好的封装,其中 InterProcessMutex 就是基于上述原理实现的互斥锁。

4.1 引入依赖

首先在 Maven 项目中引入 Curator 相关依赖:

xml 复制代码
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.5.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>5.5.0</version>
</dependency>

4.2 使用 InterProcessMutex 加锁

下面是一个完整的示例,演示如何创建 ZooKeeper 客户端并使用分布式锁保护临界区:

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 ZooKeeperLockDemo {

    private static final String ZK_ADDRESS = "127.0.0.1:2181";
    private static final String LOCK_PATH = "/locks/my-lock";

    public static void main(String[] args) throws Exception {
        // 1. 创建 ZooKeeper 客户端,带重试策略
        CuratorFramework client = CuratorFrameworkFactory.newClient(
                ZK_ADDRESS,
                new ExponentialBackoffRetry(1000, 3));
        client.start();

        // 2. 创建分布式锁
        InterProcessMutex lock = new InterProcessMutex(client, LOCK_PATH);

        try {
            // 3. 获取锁
            if (lock.acquire(10, java.util.concurrent.TimeUnit.SECONDS)) {
                System.out.println("获取锁成功,开始执行业务逻辑");
                // 模拟业务处理
                Thread.sleep(3000);
            } else {
                System.out.println("获取锁超时");
            }
        } finally {
            // 4. 释放锁
            lock.release();
            client.close();
        }
    }
}

4.3 关键点说明

  • 重试策略 :ExponentialBackoffRetry 会在连接失败时按指数退避重试,避免瞬时抖动导致会话中断。
  • 锁路径 :LOCK_PATH 是锁在 ZooKeeper 中的根路径,Curator 会在该路径下自动创建临时顺序节点。
  • 超时控制 :acquire 方法支持传入超时时间,避免因 ZooKeeper 不可用而无限阻塞。
  • 释放锁 :release 方法会删除当前客户端创建的临时节点,从而唤醒等待中的下一个客户端。

5. 常见问题与注意事项

5.1 会话超时与锁自动释放

由于锁节点是临时节点,当客户端与 ZooKeeper 的会话超时或客户端崩溃时,临时节点会自动删除,锁会被自动释放。这避免了死锁问题,但也带来一个隐患:如果业务执行时间过长,超过会话超时时间,锁可能被提前释放,导致其他客户端同时进入临界区。因此,需要合理设置会话超时时间,并尽量缩短业务执行时间。

5.2 惊群效应

如果所有等待锁的客户端都监听锁目录下的全部子节点,那么一旦锁被释放,所有客户端都会被唤醒,但只有一个能抢到锁,其余客户端需要再次进入等待,这就是惊群效应。本文介绍的「监听前一个节点」方案,每个客户端只关注自己的前驱节点,能够有效避免惊群效应。

5.3 锁的可重入性

Curator 的 InterProcessMutex 支持可重入,即同一个客户端在持有锁的情况下可以再次获取同一把锁。这在递归调用或嵌套业务场景中非常有用。

6. 总结

ZooKeeper 分布式锁通过临时顺序节点和 Watcher 机制,实现了跨节点的互斥访问。相比 Redis 分布式锁,ZooKeeper 方案在锁的自动释放和一致性方面更加可靠,但部署和维护 ZooKeeper 集群的成本也更高。在实际项目中,应根据业务场景、可用性和性能要求,选择合适的分布式锁方案。

如果希望进一步了解 Curator 的更多锁类型,例如读写锁 InterProcessReadWriteLock 或信号量 InterProcessSemaphoreMutex,可以参考官方文档继续深入。

相关推荐
马剑威(威哥爱编程)1 小时前
【AI全栈后端12-12】Spring Boot 3.x 到 4.x 迁移实操:Jakarta 11、Jackson 3 与 AI 2.0
java·开发语言·spring boot·后端
Gauss松鼠会1 小时前
【GaussDB】破除gaussdb ugin索引支持中文模糊查询的迷思-字符序
java·运维·服务器·网络·数据库·gaussdb·经验总结
IT研究室1 小时前
最新计算机毕业设计选题推荐-基于spring boot的心桥·心理健康综合服务平台-网站-文档指导-Java-springboot
java·spring boot·课程设计
Sarvartha2 小时前
Object 类
java·开发语言
知守观2 小时前
feign-core 就在依赖树里,运行时却找不到类:一次 provided scope 引发的启动失败排查
java·spring cloud·maven
她的男孩2 小时前
企业接口照样拦得住:独立 Flyway、@RequiresFeature 与离线许可证
java·spring boot·后端
十年Java程序媛2 小时前
Lambda 与函数式接口|别只会复制 ()->{},底层规则和坑一次性讲清
java·spring boot·后端
Nozokime2 小时前
ThreadLocal 在流式 Agent 里静默失效:LangChain4j 轨迹埋点的零侵入方案
java
小宋10212 小时前
OpenTelemetry GenAI可观测性实战:串起模型、工具、Token与错误
java·人工智能·算法·贪心算法