基于 Zookeeper 实现分布式锁

news2025/1/15 17:45:42

文章目录

  • 前言
  • 声明
  • 前置知识
    • 分布式锁设计原则
    • Zookeeper
  • 分布式锁实现
  • Curator框架实现分布式锁
  • 总结

前言

在分布式系统中,确保数据的一致性和避免冲突是一个核心问题,通常我们通过分布式锁来解决,分布式锁本质是一种同步机制,用于控制对共享资源或临界区的访问。

Zookeeper 作为分布式协调服务,为分布式锁的实现提供了一个有效的平台,本文将通过一个简单的示例介绍如何基于 Zookeeper 提供的接口和机制实现分布式锁。

声明

文章中所提供的代码仅供参考,旨在为开发人员提供一种实用的分布式锁实现方法,并帮助读者理解如何利用Zookeeper的特性和机制来管理分布式系统中的锁。请注意,这些代码并不适用于实际应用中

前置知识

分布式锁设计原则

实现一个分布式锁要满足以下几个基本要求:

  1. 互斥性/排他性:在同一时刻只允许一个客户端持有锁。
  2. 可用性:在客户端出现异常时,锁可以被正常释放,避免死锁。
  3. 同源性:锁不能被别的线程释放,否则会破坏互斥性/排他性。
  4. 可重入性:同一个客户端可以重复、递归调用该锁而不发生死锁。

除此之外,还要考虑在没有获得锁之前,客户端阻塞等待还是视为获取失败,这个取决于业务场景。

Zookeeper

Zookeeper 是一个传统的分布式协调服务,它更多的被用来作为一个协调器使用,比如来协调管理 Hadoop 集群、协调 Kafka 的 leader 选举等。

Zookeeper的哪些特性和机制可以高效的实现分布式锁的要求?

  1. 临时节点:临时节点的生命周期依赖创建它的会话,当会话结束后,临时节点就会被删除。此特性可以满足分布式锁的可用性。
  2. 顺序节点:在创建顺序节点时,Zookeeper会分配一个递增的计数器,排在最前面的节可以获取到锁。此特性可以实现公平锁。(没有基础开发人员可以这么理解创建节点:向同一目录创建一个节点即为获取锁。)
  3. Watcher机制:通过Watcher机制当前节点可以监听前一个节点的变化,在前一个节点删除时当前节点可以得知锁被释放,从而获取到锁。
  4. 节点数据:创建节点时设置客户端会话唯一标识为值,可以实现可重入性。

而互斥性/排他性、同源性需要通过客户端控制,代码示例中会说明。

分布式锁实现

创建一个Maven项目,导入zkclient依赖即可开始编码

<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.10</version>
</dependency>

下面的示例代码满足了分布式锁的基本要求,属于可阻塞的分布式锁,也就是在没有获得锁之前,客户端阻塞等待。

public class DistributedLock {

    private ZooKeeper client;

    // 连接信息
    private String connectString = "127.0.0.1:2181";

    // 超时时间
    private int sessionTimeOut = 30000;

    // 等待zk连接成功
    private CountDownLatch countDownLatch = new CountDownLatch(1);

    // 等待节点变化
    private CountDownLatch waitLatch = new CountDownLatch(1);

    //当前节点
    private String currentNode;

    //前一个节点路径
    private String waitPath;

    private final String ROOT_PATH = "/locks";

    //1. 在构造方法中获取连接
    public DistributedLock() throws Exception {
        client = new ZooKeeper(connectString, sessionTimeOut, watchedEvent -> {
            //  连上ZK,可以释放
            if (watchedEvent.getState() == Watcher.Event.KeeperState.SyncConnected) {
                countDownLatch.countDown();
            }

            //waitLatch 需要释放 (节点被删除并且删除的是前一个节点)
            if (watchedEvent.getType() == Watcher.Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)) {
                waitLatch.countDown();
            }
        });

        //等待Zookeeper连接成功,连接完成继续往下走
        countDownLatch.await();
        //2. 判断节点是否存在
        Stat stat = client.exists(ROOT_PATH, false);
        if (stat == null) {
            //创建一下根节点
            client.create(ROOT_PATH, ROOT_PATH.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);


        }

    }

    //3.对ZK加锁
    public boolean zkLock() {

        try {
            String sessionId = String.valueOf(client.getSessionId());
            List<String> children = client.getChildren(ROOT_PATH, false);
            if (!children.isEmpty()) {
                Collections.sort(children);
                String path = children.get(0);
                byte[] data = client.getData(ROOT_PATH + "/" + path, false, null);
                //最小序号节点是当前客户端创建的不用再次获取
                if (sessionId.equals(new String(data))) {
                    System.out.println("重入锁");
                    return true;
                }
            }

            //创建 临时带序号节点,将当前客户端id作为值,实现可重入
            currentNode = client.create(ROOT_PATH + "/seq-", sessionId.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            children = client.getChildren(ROOT_PATH, false);
            //如果创建的节点只有一个值,就直接获取到锁,如果不是,监听它前一个节点
            if (children.size() == 1) {
                return false;
            } else {
                //先排序
                Collections.sort(children);

                //获取节点名称
                String nodeName = currentNode.substring((ROOT_PATH + "/").length());

                //通过名称获取该节点在集合的位置
                int index = children.indexOf(nodeName);

                if (index == -1) {
                    System.out.println("数据异常,nodeName:" + nodeName);
                    return false;
                } else if (index == 0) {
                    //创建的节点是否是最小序号节点,如果是 就获取到锁;如果不是就监听前一个节点
                    return true;
                } else {
                    //需要监听前一个节点变化
                    waitPath = ROOT_PATH + "/" + children.get(index - 1);
                    client.getData(waitPath, true, null);

                    //等待监听执行
                    waitLatch.await();
                    return true;
                }
            }

        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    public void unZkLock() throws KeeperException, InterruptedException {
        //删除节点
        client.delete(currentNode, -1);
    }


}

示例中创建了一个名为/locks的根节点作为锁的标识符,当客户端需要获取锁时调用zkLock(),该方法会先判断当前客户端是否已经持有锁,如果持有不创建节点(这里是实现可重入),否则会在/locks根节点创建一个临时顺序节点,当同时有多个客户端获取锁时节点目录时这样的

├── locks
│   └── seq-0000000006
│   └── seq-0000000005
│   └── seq-0000000004
│   └── seq-0000000003
│   └── seq-0000000002
│   └── seq-0000000001

如果该客户端创建的节点是最小的节点,那么成功获取到锁处理业务,否则监听前一个节点并阻塞等待,当前一个节点删除时通知该客户端获取锁。

当客户端调用unZkLock()时删除其创建的节点来释放锁,因为删除自己创建的节点,所以自然而然满足同源性。

整个流程与交互如下图

在这里插入图片描述

这里需要注意的是当某个节点发生变化时,Zookeeper会按照节点的顺序逐个通知客户端,所以当图中seq-0000000002因故障先被删除,/seq-0000000003也是需要等待/seq-0000000001被删除后才会收到seq-0000000002删除的通知,所以只用监听前一个节点被删除即可。

以上代码编写完后,在需要使用分布式锁的地方直接调用即可,代码如下:

public static void main(String[] args) {
    try {
        DistributedLock lock = new DistributedLock();
        if (lock.zkLock()) {
            System.out.println(Thread.currentThread() + "获取到锁");
            Thread.sleep(20 * 1000);
            lock.unZkLock();
            System.out.println(Thread.currentThread() + "释放锁");
        }

    } catch (InterruptedException | KeeperException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Curator框架实现分布式锁

Curator是基于Zookeeper原生API接口封装的客户端框架,解决了底层的细节开发问题,提供了一套高级API,实现了如分布式锁服务、集群领导选举、共享计数器、缓存机制、分布式队列等各种应用场景。

使用Curator实现分布式锁可以大大简化代码的编写,只需引入相关依赖,直接调用封装好的接口即可。其原理与上面所述的分布式锁实现方式类似。代码如下:

Curator相关依赖

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>4.0.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.0.0</version>
</dependency>
public static void main(String[] args) {
        CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", (i, l, retrySleeper) -> false);
        client.start();
        InterProcessMutex lock = new InterProcessMutex(client, "/locks");
        try {
            // 获取互斥锁
            lock.acquire();

            // 执行需要互斥访问的代码
            // 释放互斥锁
            lock.release();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭Curator Framework客户端
            client.close();
        }
}

InterProcessMutex 是 Curator 提供的一种分布式锁的实现,使用 InterProcessMutex 可以确保在多个进程之间对共享资源的互斥访问,从而避免数据冲突和并发问题。

总结

在实现分布式锁上Zookeeper的特性提供了很大的帮助,并且它的高可用性、强一致性使得分布式锁变得更加可靠和高效。文中提供了两种基于 Zookeeper 实现分布式方案,不论是使用 Zookeeper API 还是 Curator API ,其原理是一样的。因此,我们可以在理解其原理的基础上,直接使用这些成熟的框架。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1016572.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【AI】机器学习——支持向量机(线性模型)

支持向量机是一种二分类算法&#xff0c;通过在高维空间中构建超平面实现对样本的分类 文章目录 5.1 SVM概述5.1.1 分类 5.2 线性可分SVM5.2.1 线性可分SVM基本思想5.2.2 策略函数间隔几何间隔硬间隔最大化 5.2.3 原始算法支持向量 5.2.4 对偶形式算法1. 构造并求解对偶问题2. …

【小沐学CAD】嵌入式UI开发工具:GL Studio

文章目录 1、简介2、软件功能3、应用行业3.1 航空3.2 汽车3.3 防御3.4 工业3.5 电力与能源3.6 医疗3.7 空间3.8 科技 结语 1、简介 https://disti.com/gl-studio/ DiSTI 是 HMI 软件、虚拟驾驶舱、仪表、信息娱乐、集群显示器和嵌入式 UI 解决方案的领先提供商。 而它的GL Stu…

kubernetes部署(kubeadmin)

文章目录 1.环境准备2. 安装dokcer3.部署cri-docker4.各个节点安装kubeadm等5.整合kubelet和cri-dockerd配置cri-dockerd配置kubelet 6.初始化集群 1.环境准备 环境和软件版本 OS : ubuntu 20.04 container runtime: docker CE 20.10.22 kubernetes 1.24.17 CRI&#xff1a;cr…

atoi函数

介绍&#xff1a; 头文件: <stdlib.h> 此函数的功能是将数字字符的字符串转化为字面上的整型返回。 例如&#xff1a; char arr1[] "-12"; char arr2[] "12"; char arr3[] " -12"; char arr4[] "-12a";使用atoi 我们…

Postman的高级用法一:重新认识postman核心模块

本请求示例来自于免费天气API&#xff1a; 实况天气接口API开发指南 未来一天天气预报api - 天气API 关于Postman的核心模块 全局变量请求接口请求体预处理脚本 类似beforeTest&#xff0c;在发起请求前的预执行逻辑&#xff0c;通常是生成一些动态变量值 测试用例模块 测试者…

RK3399平台开发系列讲解(入门篇)VIM的基础命令

🚀返回专栏总目录 文章目录 一、Vim 命令速查二、其他命令三、Vim模式沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 本篇将介绍Vim相关命令。 一、Vim 命令速查 简单说明一下,这张图上展示了一个键盘。图中的“•”表示,单个字母不是完整的命令,必须再有进一步…

MFC使用MScomm32.ocx控件实现串口通信

文章目录 建立项目添加MSCOMM控件变量串口的打开和关闭串口发送和接收数据数据接收数据发送16进制数据的收发转换函数数据发送数据接收项目下载地址首先电脑应该有MSCOMM32.OCX并注册 建立项目 使用VS2005新建一个基于对话框的MFC项,在资源视图,右键窗体,插入ActiveX控件,…

基于Questasim的SystemVerilog DPI使用流程

1. 前言 DPI是Direct Programming Interface的缩写&#xff0c;它提供了SystemVerilog与其它编程语言(特别是C语言)交互的接口。它允许编程人员轻松地从SystemVerilog调用C函数&#xff0c;且在C函数也可以调用Systemverilog的函数。 DPI极大地方便了使用现有的C代码&#xf…

SpaceX预计到2022年Starlink用户将达到2000万,但最终达到了100万

SpaceX的Starlink部门还没有接近实现客户和收入的预测&#xff0c;该公司在建立卫星网络之前与投资者分享了这一点华尔街日报报道今天出版。 据报道&#xff0c;2015年的一份题为“SpaceX用来从投资者那里筹集资金”的报告预计&#xff0c;到2022年&#xff0c;Starlink的订户…

Linux安装RocketMQ

又又又....Linux裝RocketMQ。 vim命令找不到 rootlocalhost ~]# sudo vim sudo: vim&#xff1a;找不到命令 [rootlocalhost ~]# sudo yum install vim 已加载插件&#xff1a;fastestmirrorFile contains no section headers. file: file:///etc/yum.repos.d/mysql-communit…

Linux网络配置:最佳指南

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

CSS核心使用

CSS核心使用 box-sizingbox-shdowtext-shadowpositionwriting-mode box-sizing 定义计算一个元素的总高度和总宽度. 属性值 content-box 默认值,width 内容宽度,height内容的高度border-box 宽度和高度包含内容,内边距和边框 widthborderpadding内容宽度, heightborderpaddi…

嵌入式:驱动开发 Day4

作业&#xff1a;通过字符设备驱动分步注册方式编写LED驱动&#xff0c;完成设备文件和设备的绑定 驱动程序&#xff1a;myled.c #include <linux/init.h> #include <linux/module.h> #include <linux/cdev.h> #include <linux/fs.h> #include <l…

软件设计模式系列之七——原型模式

1 模式的定义 原型模式&#xff08;Prototype Pattern&#xff09;是一种创建型设计模式&#xff0c;其主要目的是通过复制现有对象来创建新对象&#xff0c;而不是使用构造函数。原型模式将对象的创建委托给原型对象&#xff0c;通过克隆&#xff08;复制&#xff09;来生成新…

大数据Flink(八十二):SQL语法的DDL:Create 子句

文章目录 SQL语法的DDL:Create 子句 一、建表语句

蓝桥杯打卡Day10

文章目录 最长ZigZag子序列最小面积子矩阵 一、最长ZigZag子序列IO链接 本题思路&#xff1a;本题是一道dp问题&#xff0c; 集合划分:只有一个a[i]或者倒数第二个元素是第j个数字并且需要是下降得到a[j]:g[j]1,状态计算f[i]max(f[i],g[j]1),这是第一种情况&#xff0c;还有一…

许可分析 license分析 第十四章

许可分析是指对软件许可证进行详细的分析和评估&#xff0c;以了解组织内部对软件许可的需求和使用情况。通过许可分析&#xff0c;可以帮助组织更好地管理和优化软件许可证的使用。以下是一些可能的许可分析方法和步骤&#xff1a; 软件许可证的软件定义&#xff1a;采用软件定…

Kotlin File FileTreeWalk walkTopDown onEnter onLeave

Kotlin File FileTreeWalk walkTopDown onEnter onLeave Python遍历文件目录os.walk_for subfolder in subfolders: print(foldername/_zhangphil的博客-CSDN博客import osfor folderName, subfolders, filenames in os.walk(rD:\code\vs_code): print(当前文件夹: folderName…

【Java 基础篇】深入理解Java递归:从小白到专家

在编程世界中&#xff0c;递归是一个经常被提及的概念。但对于初学者来说&#xff0c;它可能会感到有点神秘和复杂。本文将深入探讨Java中的递归&#xff0c;从基础概念开始&#xff0c;逐步深入&#xff0c;帮助你理解这个强大的编程工具。 什么是递归&#xff1f; 递归是一…

进程,线程,并发相关入门

进程与线程的简单理解 进程是一个独立的执行单元&#xff0c;它拥有自己的内存空间、文件句柄和系统资源.进程是操作系统层面的,每个应用运行就是一个进程.进程之间通常是隔离的&#xff0c;它们不能直接访问对方的内存空间&#xff0c;必须通过进程间通信&#xff08;IPC&…