Curator实现分布式锁(可重入 不可重入 读写 联锁 信号量 栅栏 计数器)

news2024/12/26 14:11:40

文章目录

  • 前言
  • 代码实践
    • 1. 配置
    • 2. 可重入锁InterProcessMutex
    • 3. 不可重入锁InterProcessSemaphoreMutex
    • 4. 可重入读写锁InterProcessReadWriteLock
    • 5. 联锁InterProcessMultiLock
    • 6. 信号量InterProcessSemaphoreV2
    • 7. 栅栏barrier
    • 8. 共享计数器
      • 8.1. SharedCount
      • 8.2. DistributedAtomicNumber

前言

Curator是netflix公司开源的一套zookeeper客户端,目前是Apache的顶级项目。与Zookeeper提供的原生客户端相比,Curator的抽象层次更高,简化了Zookeeper客户端的开发量。Curator解决了很多zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册wathcer和NodeExistsException 异常等。

Curator主要解决了三类问题:

  • 封装ZooKeeper client与ZooKeeper server之间的连接处理
  • 提供了一套Fluent风格的操作API
  • 提供ZooKeeper各种应用场景(recipe, 比如:分布式锁服务、集群领导选举、共享计数器、缓存机制、分布式队列等)的抽象封装,这些实现都遵循了zk的最佳实践,并考虑了各种极端情况

Curator由一系列的模块构成,对于一般开发者而言,常用的是curator-frameworkcurator-recipes

  • curator-framework:提供了常见的zk相关的底层操作
  • curator-recipes:提供了一些zk的典型使用场景的参考。本节重点关注的分布式锁就是该包提供的

代码实践

curator 4.3.0支持zookeeper 3.4.x3.5,但是需要注意curator传递进来的依赖,需要和实际服务器端使用的版本相符,以使用zookeeper 3.4.14为例。

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>4.3.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.3.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.14</version>
</dependency>

1. 配置

添加curator客户端配置:

@Configuration
public class CuratorConfig {

    @Bean
    public CuratorFramework curatorFramework(){
        // 重试策略,这里使用的是指数补偿重试策略,重试3次,初始重试间隔1000ms,每次重试之后重试间隔递增。
        RetryPolicy retry = new ExponentialBackoffRetry(1000, 3);
        // 初始化Curator客户端:指定链接信息 及 重试策略
        CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.1.111:2181", retry);
        client.start(); // 开始链接,如果不调用该方法,很多方法无法工作
        return client;
    }
}

2. 可重入锁InterProcessMutex

ReentrantJDKReentrantLock类似, 意味着同一个客户端在拥有锁的同时,可以多次获取,不会被阻塞。它是由类InterProcessMutex来实现。

// 常用构造方法
public InterProcessMutex(CuratorFramework client, String path)
// 获取锁
public void acquire();
// 带超时时间的可重入锁
public boolean acquire(long time, TimeUnit unit);
// 释放锁
public void release();

测试方法:

@Autowired
private CuratorFramework curatorFramework;

public void checkAndLock() {
     InterProcessMutex mutex = new InterProcessMutex(curatorFramework, "/curator/lock");
    try {
        // 加锁
        mutex.acquire();

        // 处理业务
        // 例如查询库存 扣减库存
        
        // this.testSub(mutex); 如想重入,则需要使用同一个InterProcessMutex对象

        // 释放锁
        mutex.release();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public void testSub(InterProcessMutex mutex) {

    try {
        mutex.acquire();
    	System.out.println("测试可重入锁。。。。");
        mutex.release();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

注意:如想重入,则需要使用同一个InterProcessMutex对象。

3. 不可重入锁InterProcessSemaphoreMutex

具体实现:InterProcessSemaphoreMutexInterProcessMutex调用方法类似,区别在于该锁是不可重入的,在同一个线程中不可重入。

public InterProcessSemaphoreMutex(CuratorFramework client, String path);
public void acquire();
public boolean acquire(long time, TimeUnit unit);
public void release();

案例:

@Autowired
private CuratorFramework curatorFramework;

public void deduct() {

    InterProcessSemaphoreMutex mutex = new InterProcessSemaphoreMutex(curatorFramework, "/curator/lock");
    try {
        mutex.acquire();

        // 处理业务
        // 例如查询库存 扣减库存

    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            mutex.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4. 可重入读写锁InterProcessReadWriteLock

类似JDKReentrantReadWriteLock。一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁。这也意味着写锁可以降级成读锁。从读锁升级成写锁是不成的。主要实现类InterProcessReadWriteLock

// 构造方法
public InterProcessReadWriteLock(CuratorFramework client, String basePath);
// 获取读锁对象
InterProcessMutex readLock();
// 获取写锁对象
InterProcessMutex writeLock();

注意:写锁在释放之前会一直阻塞请求线程,而读锁不会

public void testZkReadLock() {
    try {
        InterProcessReadWriteLock rwlock = new InterProcessReadWriteLock(curatorFramework, "/curator/rwlock");
        rwlock.readLock().acquire(10, TimeUnit.SECONDS);
        // TODO:一顿读的操作。。。。
        //rwlock.readLock().unlock();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public void testZkWriteLock() {
    try {
        InterProcessReadWriteLock rwlock = new InterProcessReadWriteLock(curatorFramework, "/curator/rwlock");
        rwlock.writeLock().acquire(10, TimeUnit.SECONDS);
        // TODO:一顿写的操作。。。。
        //rwlock.writeLock().unlock();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

5. 联锁InterProcessMultiLock

Multi Shared Lock是一个锁的容器。当调用acquire, 所有的锁都会被acquire,如果请求失败,所有的锁都会被release。同样调用release时所有的锁都被release(失败被忽略)。基本上,它就是组锁的代表,在它上面的请求释放操作都会传递给它包含的所有的锁。实现类InterProcessMultiLock

// 构造函数需要包含的锁的集合,或者一组ZooKeeper的path
public InterProcessMultiLock(List<InterProcessLock> locks);
public InterProcessMultiLock(CuratorFramework client, List<String> paths);

// 获取锁
public void acquire();
public boolean acquire(long time, TimeUnit unit);

// 释放锁
public synchronized void release();

6. 信号量InterProcessSemaphoreV2

一个计数的信号量类似JDKSemaphoreJDKSemaphore维护的一组许可(permits),而Cubator中称之为租约(Lease)。注意,所有的实例必须使用相同的numberOfLeases值。调用acquire会返回一个租约对象。客户端必须在finallyclose这些租约对象,否则这些租约会丢失掉。但是,如果客户端session由于某种原因比如crash丢掉, 那么这些客户端持有的租约会自动close, 这样其它客户端可以继续使用这些租约。主要实现类InterProcessSemaphoreV2

// 构造方法
public InterProcessSemaphoreV2(CuratorFramework client, String path, int maxLeases);

// 注意一次你可以请求多个租约,如果Semaphore当前的租约不够,则请求线程会被阻塞。
// 同时还提供了超时的重载方法
public Lease acquire();
public Collection<Lease> acquire(int qty);
public Lease acquire(long time, TimeUnit unit);
public Collection<Lease> acquire(int qty, long time, TimeUnit unit)

// 租约还可以通过下面的方式返还
public void returnAll(Collection<Lease> leases);
public void returnLease(Lease lease);

案例代码:

StockController中添加方法:

@GetMapping("test/semaphore")
public String testSemaphore(){
    this.stockService.testSemaphore();
    return "hello Semaphore";
}

StockService中添加方法:

public void testSemaphore() {
    // 设置资源量 限流的线程数
    InterProcessSemaphoreV2 semaphoreV2 = new InterProcessSemaphoreV2(curatorFramework, "/locks/semaphore", 5);
    try {
        Lease acquire = semaphoreV2.acquire();// 获取资源,获取资源成功的线程可以继续处理业务操作。否则会被阻塞住
        this.redisTemplate.opsForList().rightPush("log", "10010获取了资源,开始处理业务逻辑。" + Thread.currentThread().getName());
        TimeUnit.SECONDS.sleep(10 + new Random().nextInt(10));
        this.redisTemplate.opsForList().rightPush("log", "10010处理完业务逻辑,释放资源=====================" + Thread.currentThread().getName());
        semaphoreV2.returnLease(acquire); // 手动释放资源,后续请求线程就可以获取该资源
    } catch (Exception e) {
        e.printStackTrace();
    }
}

7. 栅栏barrier

  1. DistributedBarrier构造函数中barrierPath参数用来确定一个栅栏,只要barrierPath参数相同(路径相同)就是同一个栅栏。通常情况下栅栏的使用如下:

    1. client设置一个栅栏
    2. 其他客户端就会调用waitOnBarrier()等待栅栏移除,程序处理线程阻塞
    3. client移除栅栏,其他客户端的处理程序就会同时继续运行。

DistributedBarrier类的主要方法如下:

setBarrier() - 设置栅栏
waitOnBarrier() - 等待栅栏移除
removeBarrier() - 移除栅栏
  1. DistributedDoubleBarrier双栅栏,允许客户端在计算的开始和结束时同步。当足够的进程加入到双栅栏时,进程开始计算,当计算完成时,离开栅栏。DistributedDoubleBarrier实现了双栅栏的功能。构造函数如下:

    // client - the client
    // barrierPath - path to use
    // memberQty - the number of members in the barrier
    public DistributedDoubleBarrier(CuratorFramework client, String barrierPath, int memberQty);
    
    enter()enter(long maxWait, TimeUnit unit) - 等待同时进入栅栏
    leave()leave(long maxWait, TimeUnit unit) - 等待同时离开栅栏
    

memberQty是成员数量,当enter方法被调用时,成员被阻塞,直到所有的成员都调用了enter。当leave方法被调用时,它也阻塞调用线程,直到所有的成员都调用了leave

注意:参数memberQty的值只是一个阈值,而不是一个限制值。当等待栅栏的数量大于或等于这个值栅栏就会打开!

与栅栏(DistributedBarrier)一样,双栅栏的barrierPath参数也是用来确定是否是同一个栅栏的,双栅栏的使用情况如下:

  1. 从多个客户端在同一个路径上创建双栅栏(DistributedDoubleBarrier),然后调用enter()方法,等待栅栏数量达到memberQty时就可以进入栅栏。
  2. 栅栏数量达到memberQty,多个客户端同时停止阻塞继续运行,直到执行leave()方法,等待memberQty个数量的栅栏同时阻塞到leave()方法中。
  3. memberQty个数量的栅栏同时阻塞到leave()方法中,多个客户端的leave()方法停止阻塞,继续运行。

8. 共享计数器

利用ZooKeeper可以实现一个集群共享的计数器。只要使用相同的path就可以得到最新的计数器值, 这是由ZooKeeper的一致性保证的。Curator有两个计数器, 一个是用int来计数,一个用long来计数。

8.1. SharedCount

共享计数器SharedCount相关方法如下:

// 构造方法
public SharedCount(CuratorFramework client, String path, int seedValue);
// 获取共享计数的值
public int getCount();
// 设置共享计数的值
public void setCount(int newCount) throws Exception;
// 当版本号没有变化时,才会更新共享变量的值
public boolean  trySetCount(VersionedValue<Integer> previous, int newCount);
// 通过监听器监听共享计数的变化
public void addListener(SharedCountListener listener);
public void addListener(final SharedCountListener listener, Executor executor);
// 共享计数在使用之前必须开启
public void start() throws Exception;
// 关闭共享计数
public void close() throws IOException;

使用案例:

StockController:

@GetMapping("test/zk/share/count")
public String testZkShareCount(){
    this.stockService.testZkShareCount();
    return "hello shareData";
}

StockService:

public void testZkShareCount() {
    try {
        // 第三个参数是共享计数的初始值
        SharedCount sharedCount = new SharedCount(curatorFramework, "/curator/count", 0);
        // 启动共享计数器
        sharedCount.start();
        // 获取共享计数的值
        int count = sharedCount.getCount();
        // 修改共享计数的值
        int random = new Random().nextInt(1000);
        sharedCount.setCount(random);
        System.out.println("我获取了共享计数的初始值:" + count + ",并把计数器的值改为:" + random);
        sharedCount.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

8.2. DistributedAtomicNumber

DistributedAtomicNumber接口是分布式原子数值类型的抽象,定义了分布式原子数值类型需要提供的方法。

DistributedAtomicNumber接口有两个实现:DistributedAtomicLongDistributedAtomicInteger

在这里插入图片描述

这两个实现将各种原子操作的执行委托给了DistributedAtomicValue,所以这两种实现是类似的,只不过表示的数值类型不同而已。这里以DistributedAtomicLong 为例进行演示

DistributedAtomicLong除了计数的范围比SharedCount大了之外,比SharedCount更简单易用。它首先尝试使用乐观锁的方式设置计数器, 如果不成功(比如期间计数器已经被其它client更新了), 它使用InterProcessMutex方式来更新计数值。此计数器有一系列的操作:

  • get(): 获取当前值
  • increment():加一
  • decrement(): 减一
  • add():增加特定的值
  • subtract(): 减去特定的值
  • trySet(): 尝试设置计数值
  • forceSet(): 强制设置计数值

最后必须检查返回结果的succeeded(), 代表此操作是否成功。如果操作成功, preValue()代表操作前的值, postValue()代表操作后的值。

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

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

相关文章

再学C语言28:输入和输出——重定向和文件

默认情况下&#xff0c;使用标准I/O包的C程序将标准输入作为其输入源&#xff0c;即标识为stdin的流 stdin流是作为向计算机中读取数据的常规方式而建立&#xff0c;可以是键盘、语音等不同输入设备 现代计算机还可以从文件中需求其输入&#xff0c;而不仅仅是传统的输入设备…

【docker10】Docker容器数据卷

Docker容器数据卷 1.Docker容器数据卷是什么 注意(坑): 容器卷记得加入 --privilegedtrue 为什么: docker挂载主目录访问如果出现cannot open directory.:Permission denied 解决办法: 在挂在目录后多加一个–privilegedtrue参数即可 如果是CentOS7安全模块会比之前系统版本加…

c++ - 第20节 - 异常

1.C语言传统的处理错误的方式 传统的错误处理机制&#xff1a; 1.终止程序&#xff0c;如assert&#xff0c;缺陷&#xff1a;用户难以接受。如发生内存错误&#xff0c;除0错误时就会终止程序。2.返回错误码&#xff0c;缺陷&#xff1a;需要程序员自己去查找对应的错误。如系…

基于神将网络方式进行数据回归拟合实例

前言本篇博客主要以神经网络拟合数据这个简单例子讲起&#xff0c;然后介绍网络的保存与读取&#xff0c;以及快速新建网络的方法。一、神经网络对数据进行拟合import torch from matplotlib import pyplot as plt import torch.nn.functional as F# 自定义一个Net类&#xff0…

Diffusion model(二): 训练推导详解

接上文 Diffusion的训练推导 1. 最小化负对数似然与变分下界 在弄懂diffusion model前向和反向过程之后&#xff0c;最后我们需要了解其训练推导过程&#xff0c;即用什么loss以及为什么。在diffusion的反向过程中&#xff0c;根据(3)(3)(3)式我们需要预测μθ(xt,t),Σθ(x…

【Linux】进程状态和进程优先级

文章目录1. 进程状态2. Linux的进程状态3. 僵尸进程4. 孤儿进程5. 进程优先级1. 进程状态 为了更深入地了解进程&#xff0c;我们需要知道进程的不同状态。 不同的操作系统&#xff0c;对于进程状态有着不同的说法&#xff0c;如&#xff1a;运行、阻塞、挂起、新建、就绪、等…

SIoU Loss

1、论文 题目&#xff1a;《SIoU Loss: More Powerful Learning for Bounding Box Regression》 参考博客&#xff1a; https://blog.csdn.net/qq_56749449/article/details/125753992 2、原理 有关IoU损失函数&#xff0c;像GIoU、DIoU、CIoU都没有考虑真实框与预测框之间的…

关于zookeeper和kafka不得不说的秘密

zookeeper简介1. zookeeper的概述ZooKeeper是一个分布式的&#xff0c;开放源码的分布式应用程序协调服务&#xff0c;是Google的Chubby一个开源的实现&#xff0c;是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件&#xff0c;提供的功能包括&#xff1a…

【四】Netty 分隔符和定长解码器的应用

Netty 分隔符和定长解码器的应用理论说明LineBasedFrameDecoder 开发大概流程代码展示netty 依赖EchoServer 服务端启动类EchoServerHandlerEchoClientEchoClientHandler结果打印客户端打印服务端打印FixedLengthFrameDecoder 开发代码展示EchoServer 服务端启动类EchoFixServe…

【云原生】k8s之pod控制器

内容预知 前言 1.pod控制器的相关知识 1.1 pod控制器的作用 1.2 pod控制器的多种类型 1.3 pod容器中的有状态和无状态的对比 &#xff08;1&#xff09;有状态实例 &#xff08;2&#xff09;无状态实例 2.Deployment控制器 2.1 SatefulSet 控制器的运用 2.1 Sateful…

从0到1完成一个Vue后台管理项目(六、404页)

往期 从0到1完成一个Vue后台管理项目&#xff08;一、创建项目&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;二、使用element-ui&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;三、使用SCSS/LESS&#xff0c;安装图标库&#xff09; 从0到1完成一个Vu…

[LeetCode周赛复盘] 第 95 场周赛20230107

[LeetCode周赛复盘] 第 95 场周赛20230107 一、本周周赛总结二、 [Easy] 2525. 根据规则将箱子分类1. 题目描述2. 思路分析3. 代码实现三、[Medium] 2526. 找到数据流中的连续整数![在这里插入图片描述](https://img-blog.csdnimg.cn/237210adb20e457aaf2671e6e8f9e43b.png)2. …

Linux系统中C++多态和数据封装的基本方法

大家好&#xff0c;今天主要和大家分享一下&#xff0c;多态&#xff0c;数据封装的使用方法。 目录 第一&#xff1a;C中的多态 第二&#xff1a;C中数据封装方法 第一&#xff1a;C中的多态 C多态意味着调用成员函数时&#xff0c;会根据调用函数的对象的类型来执行不同的函…

将内核加载到内存

文章目录前言前置知识代码实验操作前言 本博客记录《操作系统真象还原》第五章第3个实验的操作~ 实验环境&#xff1a;ubuntu18.04VMware &#xff0c; Bochs下载安装 实验内容&#xff1a;将内核载入内存,初始化内核代码 实验原理 编写内核程序。将内核程序用dd命令复制到…

Odoo 16 企业版手册 - 库存管理之存储类别

存储类别 Odoo中的存储类别功能将允许您将许多存储位置分组到一个类别下。您可以在Odoo 库存管理模块中创建许多此类类别&#xff0c;这将有助于执行更智能的放置操作。在配置存储类别之前&#xff0c;您必须配置库存中可用的存储位置。然后&#xff0c;您可以将它们分组到一个…

LeetCode刷题模版:31 - 40

目录 简介31. 下一个排列32. 最长有效括号33. 搜索旋转排序数组34. 在排序数组中查找元素的第一个和最后一个位置35. 搜索插入位置36. 有效的数独37. 解数独38. 外观数列39. 组合总和40. 组合总和 II结语简介 Hello! 非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指…

电影解说开头怎么写吸引人?

电影解说开头怎么写吸引人&#xff1f;很多电影解说创作者文采不够好&#xff0c;开头不知道怎么写&#xff1f;毕竟想留住用户继续观看视频&#xff0c;开头是至关重要的&#xff0c;今天笔者就分享电影解说文案万能公式模板&#xff0c;让大家创作更简单&#xff01;一个好的…

feature engnineering 特征工程

特征工程数值型变量standardizationlog_transformation(使其符合正态分布)polynomial features分类型变量orinigalencoderonehot encoder分类创造下的数值以下代码根据Abhishek Thakur在kaggle上的机器学习30天 &#xff08;b站&#xff09; (kaggle)可惜的是&#xff0c;我没有…

Oracle 19c VLDB and Partitioning Guide 第5章:管理和维护基于时间的信息 读书笔记

本文为Oracle 19c VLDB and Partitioning Guide第5章Managing and Maintaining Time-Based Information的读书笔记。 Oracle 数据库提供了基于时间管理和维护数据的策略。 本章讨论 Oracle 数据库中的组件&#xff0c;这些组件可以构建基于时间管理和维护数据的策略。 尽管大…

计算机网络复习之网络层

文章目录数据报与虚电路服务的对比IP 协议IP数据报格式IP地址NAT&#xff08;网络地址转换&#xff09;子网划分和子网掩码在支持子网划分的因特网中&#xff0c;路由器如何转发IP数据报无分类编制CIDR构成超网RIP协议OSPF协议ARP协议ICMP协议Ping和Traceroute参考路由选择是网…