分布式锁的最佳实践之Redisson

从库存超卖问题分析锁和分布式锁的应用(一)
从库存超卖问题分析锁和分布式锁的应用(二)
分布式锁的最佳实践之Redisson

本文接从库存超卖问题分析锁和分布式锁的应用(二)讲解Redisson在分布式锁的应用实践。

官网文档地址:https://github.com/redisson/redisson/wiki

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

pom依赖:

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.17.1</version>
</dependency>

【1】可重入锁(Reentrant Lock)

基于Redis的分布式可重入锁对象用于Java,并实现了Lock接口。它使用发布/订阅(pub/sub)通道来通知所有等待获取锁的Redisson实例中的其他线程。

如果获得锁的Redisson实例崩溃,那么该锁可能会永远处于已获取状态,导致死锁。为了避免这种情况,Redisson维护了一个锁看门狗(lock watchdog),只要持有锁的Redisson实例还活着,它就会延长锁的过期时间。默认情况下,锁看门狗的超时时间为30秒,并且可以通过Config.lockWatchdogTimeout设置进行更改。

在获取锁时可以定义leaseTime参数。在指定的时间间隔后,被锁定的锁将自动释放。

RLock对象根据Java的Lock规范行为。这意味着只有锁的所有者线程才能解锁,否则会抛出IllegalMonitorStateException异常。如果需要多个线程可以同时拥有锁,则应考虑使用RSemaphore对象。

看门狗、leaseTime以及只有锁的所有者线程才能解锁对本文所有锁章节都适用。

简而言之,Redisson的RLock提供了一种机制,使得在分布式环境中多个节点能够安全地共享和控制对资源的访问,而不会出现死锁或竞争条件。通过使用Redis作为协调服务,它确保了锁的一致性和可用性,即使在某个节点失败的情况下也能维持系统运行的稳定性。

RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();// 加锁以后10秒钟自动解锁// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

① 配置

官网文档配置章节详细说明了单节点模式、哨兵模式、集群模式等情况下关于Redisson的配置。

单节点模式配置如下:

@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){Config config = new Config();// 可以用"rediss://"来启用SSL连接config.useSingleServer().setAddress("redis://127.0.0.1:6379");//... 还可以设置其他参数比如用户名、密码、连接数等return Redisson.create(config);}
}

② 锁实践

修改我们的代码如下:

@Autowired
private RedissonClient redissonClient;public void deduct() {// 加锁,获取锁失败重试RLock lock = this.redissonClient.getLock("lock");lock.lock();try {// 1. 查询库存信息String stock = redisTemplate.opsForValue().get("stock");// 2. 判断库存是否充足if (stock != null && stock.length() != 0) {int st = Integer.parseInt(stock);if (st > 0) {// 3.扣减库存redisTemplate.opsForValue().set("stock", String.valueOf(--st));}}} finally {// 释放锁lock.unlock();}
}

这里获取的是RedissonLock实例,其实现了可重入(底层是lua脚本)和定期续约(底层是时间轮定时器+lua脚本)功能。其解锁的核心代码如下所示,在解锁成功后采用了发布/订阅的机制来通知其他阻塞该锁的线程来获取锁

protected RFuture<Boolean> unlockInnerAsync(long threadId) {return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +"return nil;" +"end; " +"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +"if (counter > 0) then " +"redis.call('pexpire', KEYS[1], ARGV[2]); " +"return 0; " +"else " +"redis.call('del', KEYS[1]); " +"redis.call('publish', KEYS[2], ARGV[1]); " +"return 1; " +"end; " +"return nil;",Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

③ 看门狗如何解决死锁问题

当一个Redisson实例获得了锁并突然崩溃,其内部的锁看门狗机制会防止锁永久持有,从而避免潜在的死锁情况。看门狗的工作原理如下:

  1. 初始设置锁过期时间
    当一个Redisson实例成功获取锁时,它会在Redis中为这个锁设置一个过期时间(TTL)。这个过期时间是锁的租约时间,即leaseTime参数,加上一个额外的安全缓冲时间,通常由lockWatchdogTimeout配置项决定,默认是30秒。

  2. 看门狗续租
    成功获取锁的Redisson实例会启动一个后台线程,即看门狗,它会周期性地检查锁的持有者是否仍然活跃。如果Redisson实例正常运行,看门狗会在锁的过期时间到达之前,自动更新锁的过期时间,以保持锁的有效性。这个操作会持续进行,直到锁的持有者主动释放锁或Redisson实例本身停止运行。

  3. 检测实例崩溃
    如果持有锁的Redisson实例崩溃,它的看门狗也将随之停止工作,不再能更新锁的过期时间。因此,一旦超过锁的总过期时间(leaseTime + lockWatchdogTimeout),锁将在Redis中自动过期并被释放。

  4. 其他实例尝试获取锁
    锁一旦释放,其他正在等待的Redisson实例可以尝试获取这个锁。由于锁的过期,它们将有机会成为新的锁持有者,从而继续执行其任务,避免了死锁的发生。

通过这种方式,即使某个Redisson实例崩溃,系统也能够自动恢复并允许其他实例继续正常工作,确保分布式系统的健壮性和一致性。

【2】公平锁

基于Redis的Redisson分布式可重入公平锁也是实现了 java.util.concurrent.locks.Lock 接口的一种 RLock 对象。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。

其同样可以指定过期时间,在获取锁时,可以定义leaseTime参数。在指定的时间间隔后,被锁定的锁将自动释放。

RLock lock = redisson.getFairLock("myLock");// traditional lock method
lock.lock();// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {try {...} finally {lock.unlock();}
}

【3】红锁

基于Redis的Redisson红锁 RedissonRedLock 对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个 RLock 对象关联为一个红锁,每个 RLock 对象实例可以来自于不同的Redisson实例。

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();

目前已经过期,可以使用RLock or RFencedLock 替代。

【4】读写锁

基于Redis的分布式可重入读写锁(ReadWriteLock)对象为Java实现了ReadWriteLock接口。其中,读锁(ReadLock)和写锁(WriteLock)都实现了RLock接口。

该读写锁允许多个读锁持有者和仅一个写锁持有者存在。

如果获得锁的Redisson实例崩溃,那么该锁可能永远保持在获取状态,导致死锁。为了避免这种情况,Redisson维护了一个锁看门狗,只要持有锁的Redisson实例还活着,它就会延长锁的过期时间。默认情况下,锁看门狗的超时时间是30秒,这个值可以通过Config.lockWatchdogTimeout设置来更改。

此外,Redisson允许在获取锁时指定leaseTime参数。在指定的时间间隔后,被锁定的锁将自动释放。

RLock对象遵循Java的Lock规范。这意味着只有锁的所有者线程才能解锁,否则会抛出IllegalMonitorStateException异常。如果需要多个线程可以同时拥有锁,那么应该考虑使用RSemaphore对象。

总结来说,Redisson提供的分布式读写锁确保了读操作可以并发进行,而写操作具有排他性。通过锁看门狗和租约时间的机制,它不仅提供了锁的自动释放功能,还确保了即使在持有锁的实例发生故障时,系统也能避免死锁,保持一致性和可用性。

RReadWriteLock rwlock = redisson.getReadWriteLock("myLock");// RedissonReadLock
RLock lock = rwlock.readLock();// RedissonWriteLock
RLock lock = rwlock.writeLock();// traditional lock method
lock.lock();// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);// or wait for lock aquisition up to 100 seconds 
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {try {...} finally {lock.unlock();}
}

比如我们有两个请求,一个读,一个写,分别对应不同的方法逻辑:

public String testRead() {RReadWriteLock rwLock = this.redissonClient.getReadWriteLock("rwLock");rwLock.readLock().lock(10, TimeUnit.SECONDS);System.out.println("测试读锁。。。。");// rwLock.readLock().unlock();return null;
}public String testWrite() {RReadWriteLock rwLock = this.redissonClient.getReadWriteLock("rwLock");rwLock.writeLock().lock(10, TimeUnit.SECONDS);System.out.println("测试写锁。。。。");// rwLock.writeLock().unlock();return null;
}

打开两个浏览器窗口测试:

  • 同时访问写:一个写完之后,等待一会儿(约10s),另一个写开始
  • 同时访问读:不用等待
  • 先写后读:读要等待(约10s)写完成
  • 先读后写:写要等待(约10s)读完成

【5】信号量

基于Redis的Redisson的分布式信号量(Semaphore)Java对象 RSemaphore 采用了与java.util.concurrent.Semaphore 相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

acquire方法用来获取一个信号资源,release方法用来释放前面获取的资源,当获取不到资源时将阻塞等待其他线程释放资源。

JUC的Semaphore可以解决服务实例内部/方法内部的资源竞争问题,但是无法解决分布式场景下的资源竞争问题。Redisson的分布式信号量RSemaphore 可以解决分布式场景下资源竞争问题。

示例如下:

RSemaphore semaphore = redisson.getSemaphore("mySemaphore");//尝试设置许可数量,如果设置成功返回true,如果已经被设置过,返回false。
boolean  res = trySetPermits(3);// acquire single permit 获取一个许可
semaphore.acquire();// or acquire 10 permits 获取10个许可
semaphore.acquire(10);// or try to acquire permit 尝试获取一个许可
boolean res = semaphore.tryAcquire();// or try to acquire permit or wait up to 15 seconds 尝试获取许可,15秒后自动放弃
boolean res = semaphore.tryAcquire(15, TimeUnit.SECONDS);// or try to acquire 10 permit 尝试获取10个许可
boolean res = semaphore.tryAcquire(10);// or try to acquire 10 permits or wait up to 15 seconds
//尝试获取10个许可,等待15秒后自动放弃
boolean res = semaphore.tryAcquire(10, 15, TimeUnit.SECONDS);
if (res) {try {...} finally {// 释放资源semaphore.release();}
}

【6】闭锁(CountDownLatch)

基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。

在使用之前应通过trySetCount(count)方法进行初始化。

代码使用示例如下:

RCountDownLatch latch = redisson.getCountDownLatch("myCountDownLatch");latch.trySetCount(1);// await for count down -阻塞等待计数减减
latch.await();// in other thread or JVM
RCountDownLatch latch = redisson.getCountDownLatch("myCountDownLatch");
// 计数-1 ,为0 时会唤醒阻塞线程
latch.countDown();

闭锁又称倒计时器,await方法用来阻塞某个线程,countDown方法用来进行计数-1,当count减为0时,将唤醒await阻塞的方法。其适用于某个线程等待一组子线程完成(或者是发生、处理中)任务的场景。

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

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

相关文章

重测序数据处理得到vcf文件

重测序数据处理得到vcf文件 文章目录 重测序数据处理前言1. 数据是rawdata&#xff0c;需用fastp对数据进行质控和过滤2. 利用getorganelle软件组装叶绿体基因组3. 检查基因组大小&#xff0c;确认是否完整&#xff0c;然后和已知的红毛菜科叶绿体基因组一起构树4. 根据树形结果…

微积分-微分应用2(平均值定理)

要得出平均值定理&#xff0c;我们首先需要以下结果。 罗尔定理 设函数 f f f 满足以下三个假设&#xff1a; f f f 在闭区间 [ a , b ] [a, b] [a,b] 上连续。 f f f 在开区间 ( a , b ) (a, b) (a,b) 上可导。 f ( a ) f ( b ) f(a) f(b) f(a)f(b) 则在开区间 ( a , b …

CTFHUB-SQL注入-UA注入

目录 判断是否存在注入 判断字段数量 判断回显位置 查询数据库名 查询数据库下的表名 查询表中的字段名 查询字段名下的数据 由于本关是UA注入&#xff0c;就不浪费时间判断是什么注入了&#xff0c;在该页面使用 burp工具 抓包&#xff0c;修改User-Agent&#xff0c;加…

JavaScript之Web APIs-DOM

目录 DOM获取元素一、Web API 基本认知1.1 变量声明1.2 作用和分类1.3 DOM树1.4 DOM对象 二、获取DOM对象2.1 通过CSS选择器来获取DOM元素2.2 通过其他方式来获取DOM元素 三、操作元素内容3.1 元素.innerTest属性3.2 元素.innerHTML属性 四、操作元素属性4.1 操作元素常用属性4…

图形编辑器基于Paper.js教程09:鼠标拖动画布,以鼠标点为缩放中心进行视图的缩放

如何使用Paper.js实现画布的缩放与拖动功能 在Web开发中&#xff0c;利用Paper.js库进行图形的绘制和交互操作是一种常见的实践。Paper.js是一个强大的矢量图形库&#xff0c;可以让开发者通过简洁的API完成复杂的图形操作。在本文中&#xff0c;我们将详细探讨如何使用Paper.…

autohotkey自动化执行vim命令

开发原因 首先讲一下为什么用这个自动化执行脚本? 存在的问题: vim作为linux自带唯一的编辑器, 开发时, 不得不用, 但是他的按键模式复杂, 就比如最简单的复制黏贴, 都需要按下好几次esc按键和插入, 极大的增加了初学者的学习成本, 并且在掌握了更快的键盘方案后, 就感觉vi…

昇思25天学习打卡营第29天 | 基于MindSpore通过GPT实现情感分类

基于MindSpore框架通过GPT模型实现情感分类展示了从项目设置、数据预处理到模型训练和评估的详细步骤&#xff0c;提供了一个完整的案例来理解如何在自然语言处理任务中实现情感分析。 首先&#xff0c;环境配置是任何机器学习项目的起点。项目通过安装特定版本的MindSpore和相…

Linux:使用vim编辑文件为什么会影响目录的mtime

一个有趣的现象 最近在调试一个问题时&#xff0c;发现了一个有趣的现象&#xff1a;touch一个存在的文件&#xff0c;文件的mtime发生了更新&#xff0c;文件所在目录的mtime不会更新&#xff1b;而使用vim编辑这个文件后再保存&#xff0c;文件和文件所在目录的mtime都会被更…

kotlin中常见的创建协程的方式

以下是kotlin开发中一些最常见的创建协程的方式&#xff1a; 1. 使用CoroutineScope.launch 这是最常见的启动协程的方式&#xff0c;通常用于不需要返回结果的协程。它返回一个Job对象&#xff0c;可以用来管理协程的生命周期。 val scope CoroutineScope(Dispatchers.Def…

未来已来:生成式 AI 在对话系统与自主代理中的探索

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;日常聊聊 ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 一、整体介绍 对话系统&#xff08;Chat&#xff09; 自主代理&#xff08;Agent&#xff09; 二、技术对比 技术差异 优…

安装anaconda后jupyter notebook打不开 闪退

首先&#xff0c;通过清华源安装了最新的anaconda&#xff08;安装在了D盘&#xff09; 尝试打开jupyter&#xff0c;发现小黑框1s后自己关了&#xff0c;根本不打开浏览器 之后尝试按照这个做了一遍https://blog.csdn.net/gary101818/article/details/123560304还是不行。。…

Nagios高频面试题及参考答案(2万字长文)

目录 什么是Nagios?它的主要功能是什么? Nagios可以监控哪些类型的资源? 描述Nagios的架构 Nagios如何处理高可用性? 解释Nagios中的“被动检查”和“主动检查” Nagios中有哪些主要的服务状态? Nagios配置文件的结构是什么样的? 描述Nagios的核心组件 如何在Na…

【BUG】已解决:TypeError: Descriptors cannot not be created directly.

已解决&#xff1a;TypeError: Descriptors cannot not be created directly. 目录 已解决&#xff1a;TypeError: Descriptors cannot not be created directly. 【常见模块错误】 【错误原因】 【解决方案】 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来…

创建和管理大量的数据对象:ScriptableObject

一、创建一个继承自ScriptableObject&#xff0c;名为ItemData的类 1、ItemData.cs using UnityEngine;[CreateAssetMenu(menuName "Items/Item")] public class ItemData : ScriptableObject {public string description;public Sprite thumbnail;public GameObj…

数据挖掘-分类和预测

来自&#x1f96c;&#x1f436;程序员 Truraly | 田园 的博客&#xff0c;最新文章首发于&#xff1a;田园幻想乡 | 原文链接 | github &#xff08;欢迎关注&#xff09; 文章目录 概念KNN 算法决策树ID3 算法缺点 C4.5 算法CART 算法 贝叶斯算法朴素贝叶斯算法贝叶斯信念网络…

如何做好结构化逻辑分析:分析之万能公式

有人问&#xff1a;我看了很多书&#xff0c;可是一到分析问题和解决问题时&#xff0c;还是不知如何下手。你能给我一个万能框架吗&#xff1f;这样我遇到问题就可以马上找到思路、直接套用&#xff0c;再也不用让大脑去神游和不知所措了。 我想了想&#xff0c;总结出了这个…

突破•指针二

听说这是目录哦 复习review❤️野指针&#x1fae7;assert断言&#x1fae7;assert的神奇之处 指针的使用和传址调用&#x1fae7;数组名的理解&#x1fae7;理解整个数组和数组首元素地址的区别 使用指针访问数组&#x1fae7;一维数组传参的本质&#x1fae7;二级指针&#x…

2-41 基于matlab的小车倒立摆系统的控制及GUI动画演示

基于matlab的小车倒立摆系统的控制及GUI动画演示。输入小车及倒立摆的初始参数&#xff0c;位置参数&#xff0c;对仿真时间和步长进行设置&#xff0c;通过LQR计算K值&#xff0c;进行角度、角速度、位置、速度仿真及曲线输出&#xff0c;程序已调通&#xff0c;可直接运行。 …

Linux设置开机启动Nginx

设置开机启动项 systemctl enable nginx 这个命令会创建一个符号链接到/etc/systemd/system/目录下的multi-user.target.wants/目录&#xff0c;从而确保Nginx在系统启动时自动运行。 查看Nginx是否已设置开机启动项 systemctl list-unit-files | grep nginx 删除开机启动 …

数据无忧:2024年高效硬盘数据恢复解决方法

在这个数字化时代&#xff0c;数据已成为我们生活与工作中不可或缺的一部分。手机或者电脑不够存储数据的时候我们最常用的就是采购硬盘来存储。以备不时之需我们来学习一下硬盘数据恢复的一些技巧吧。 1.福、昕数据恢复 这工具是一款简单小巧的数据恢复工具。下载安装在一分…