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,可以参考官方文档继续深入。

相关推荐
步行cgn17 分钟前
FormHttpMessageConverter 详解:Spring MVC 中表单数据的读写转换器
java·spring·mvc
龙亘川23 分钟前
构建城市公共安全数字底座:亘川智城智慧安防系统全维度解读
java·大数据·开发语言·人工智能·信息可视化
金銀銅鐵26 分钟前
[Java] 用 CLI 展示 EnumMap 增删键值对时的内部变化
java·源码·ai编程
秋天的怀念_David1 小时前
第二十二夜 · 数字迷雾
java·程序人生·算法
Raas1001 小时前
AI网关部署方案?从部署到上线全流程
java·开发语言·人工智能·gateway·ai网关·企业级ai网关·企业级ai产品
niucloud-admin2 小时前
NIUCLOUD PHP SAAS上门家政使用文档:城市管理
java·前端·php
霸道流氓气质2 小时前
智能客服组件:分层意图识别、RAG Advisor 与人工转接状态机的 Vue3 + Spring AI 生产级实战
java·人工智能·spring
captain3763 小时前
MyBatis操作数据库(入门2)
java·数据库·spring boot·后端·spring·mybatis
心中有你02143 小时前
Java实现Dijkstra最短路径算法(邻接表 + 优先队列)
java·数据库·算法
步行cgn4 小时前
Spring MVC + Thymeleaf 模板解析失败:从 500 错误到页面正常显示
java·spring·mvc