【工作技术栈】基于注解的redis分布式锁(支持SPEL细粒度+redisson可重入功能)

这里写目录标题

  • 前言
  • 基于注解的reids分布式锁
  • 感悟

前言

刚开始我们使用的redis工具是自己写的,因为觉得redisson没必要(其实是没有人想因为自己不懂redisson导致线上问题吧。。。毕竟公共组件)

这个就是目前我们用的,手写简易lua脚本实现的redis分布式锁
https://blog.csdn.net/qq_39760347/article/details/132773766
确实也够用了,因为业务上面用到的复杂场景并不多,或者说。。。看下去就明白了

恰好这次有一个需求是我来接,大致介绍一下:
首先有一个大的蓄水池(化名),当用户要喝水的时候,就从蓄水池拿出来一部分(订购),如果没用完就将剩下的水倒回去(退订),很显然多个用户对蓄水池进行操作的时候需要分布式锁的支持才能保证蓄水池中的水不会出现数量的错误。
其次,用户在用水的期间觉得自己的碗有点小了,所以就想对自己的碗进行扩容(扩容),如果觉得自己碗大了(太贵),就想对自己的碗进行缩容,那么扩容和缩容过程中少的水和多的水需要蓄水池的支持,这个业务场景同样需要蓄水池的支持(多退少补)。

OK,业务场景就这么"简单",接下来开始设计:
首先每一个用户线程操作蓄水池的时候,计算机的操作过程就是,查 - 增加/减少 - 写,很显然这三个步骤需要原子性,并且用户线程之间的操作需要互斥的前提下才能正常运行。资源互斥,上锁!由于业务场景不仅仅对单个字段进行修改,同时存在众多其他相关业务(账单等等),因此mysql的写锁[for update]无法使用了。

接下来分析用户线程类型:
用户线程分三种类型:
1、新来的(订购),用新的完接水的,仅使用订购方法(减少蓄水池操作)
2、要走的(退订),用完了要归还剩下的,仅使用退订方法(增加蓄水池操作)
3、要更换的(修改),用了一半觉得碗大了或者小了,想换碗规格(扩容、缩容操作)

这里三个操作都需要互斥,所以分布式锁的标志key相同。

用户线程3的设计就是,先2,后1,也就是退订原来的,订购新规格,所以存在一个场景,进入3的临界区时,调用1和2的时候会再次加锁。
如果看了之前的分布式锁的blog的同学会发现,如果再次加锁,很显然重新生成uuid的话会让redis认为是另外一个线程,这样必会死锁。
这个时候需要锁的可重入性,需要重新设计了。。。

基于注解的reids分布式锁

DistributeLock.java

package top.swzhao.project.workflow.common.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @Discreption <> 分布式锁注解*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DistributeLock {/*** 临界区标识,支持SPEL表达式* @return*/String key() default "";/*** 锁过期时间(s)* @return*/int timeout() default 10;/*** 是否是互斥场景* true:取号器场景,等待自旋* false:互斥场景,执行后不再执行* @return*/boolean loopWithLockFail() default false;/*** 自旋时间(s)* @return*/int tryTime() default 100;
}

DistributeLockAspect.java

package top.swzhao.project.workflow.common.annotation.aspect;import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import top.swzhao.project.workflow.common.annotation.DistributeLock;
import top.swzhao.project.workflow.common.utils.RedisUtil;import java.util.Objects;
import java.util.UUID;/*** @Discreption <>*/
@Aspect
@Component
@Slf4j
public class DistributeLockAspect {/*** 当前线程标识UUID*/private static final ThreadLocal<String> uniqueIdThreadLocal = new ThreadLocal<>();@Autowiredprivate RedissonClient redissonClient;@Pointcut(value = "@annotation(top.swzhao.project.workflow.common.annotation.DistributeLock)")public void scheduleLock(){}@Around(value = "scheduleLock() && @annotation(lock)")public Object around(ProceedingJoinPoint proceedingJoinPoint, DistributeLock lock) {// 解析当前需要加锁的业务标识String key = generateKeyBySpEL(lock.key(), proceedingJoinPoint);// 锁过期的时间int timeout = lock.timeout();// 是否自旋boolean isLoop = lock.loopWithLockFail();// 自选尝试次数int loopTime = lock.tryTime();boolean setResult = false;RLock rLock = redissonClient.getLock(key);try {if (rLock.isLocked() && !isLoop) {// 不再尝试直接退出log.info("当前线程没有获取到分布式锁[key:{}],锁已被其他线程占用,退出...", key);return null;}// 到这里要么是场景没有锁,要么就是需要自旋获取锁boolean success = rLock.tryLock(loopTime, timeout, TimeUnit.SECONDS);if (!success) {log.warn("分布式锁获取失败!请检查key:{} 是否长时间未释放!", key);throw new Exception("分布式锁抢占失败");}return proceedingJoinPoint.proceed();} catch (Exception e) {// 这里是锁异常,正常业务异常需要提前捕获并抛出log.error("分布式锁加锁异常:", e);throw e;} catch (Throwable throwable) {throwable.printStackTrace();throw throwable;} finally {// 释放锁if (Objects.nonNull(rLock) && rLock.isHeldByCurrentThread()) {rLock.unlock();}}}private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();private DefaultParameterNameDiscoverer defaultParameterNameDiscoverer = new DefaultParameterNameDiscoverer();/*** 将入参替换到key中的占位符* @param key* @param proceedingJoinPoint* @return*/private String generateKeyBySpEL(String key, ProceedingJoinPoint proceedingJoinPoint) {// 转换表达式Expression expression = spelExpressionParser.parseExpression(key);// 获取上下文EvaluationContext context = new StandardEvaluationContext();// 从当前方法中获取用户输入的参数MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();// 注解方法中的入参Object[] args = proceedingJoinPoint.getArgs();// 从方法中获取未被压缩过的参数名:正常来说压缩过的为arg1,arg2,这样找不到用户的真实参数名称String[] parameterNames = defaultParameterNameDiscoverer.getParameterNames(signature.getMethod());if (parameterNames == null || parameterNames.length == 0) {return key;}// 判断是否存在spel表达式boolean flag = false;for (int i = 0; i < parameterNames.length; i++) {if (StringUtils.isNotBlank(parameterNames[i]) && key.contains(parameterNames[i])) {flag = true;context.setVariable(parameterNames[i], args);}}return !flag ? key : Objects.requireNonNull(expression.getValue(context).toString());}}

感悟

首先可重入的场景我以为在我目前的项目中应该不会遇到的,其实确实实现方案有很多,其中有一个方案就是将三个步骤都写在一个class中,这样3步骤调用1和2的时候可以不用通过代理进来,也就不会走aspect,确实能够解决这个问题,但是我觉得不能完全解决,所以最终还是决定了改造我们的redis分布式锁。
改造完成后我发现redisson并不是仅仅只有分布式锁的功能,它基本上覆盖了java中所有的数据结构,也就是将java的内存存储扩展到了redis中,如果你愿意,你可以将所有的对象存储在redis中,将redis作为你的java堆,这样能够在集群的场景下,java应用之间能够数据共享,但也只有部分场景需要这么用,毕竟是远程调用。
最后,redisson的可重入锁的设计思想,我认为就是一种操作系统的信号量思想,redisson存储的value是一个hash,key存储了当前线程,value存储了当前线程加锁的次数。不论是java中的ReentrentLock,还是其他的可重入机制,基本上都延续了操作系统的进程同步原理,所以万变不离其宗,计算机基础是有用的!

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

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

相关文章

python222网站实战(SpringBoot+SpringSecurity+MybatisPlus+thymeleaf+layui)-菜单管理实现

锋哥原创的SpringbootLayui python222网站实战&#xff1a; python222网站实战课程视频教程&#xff08;SpringBootPython爬虫实战&#xff09; ( 火爆连载更新中... )_哔哩哔哩_bilibilipython222网站实战课程视频教程&#xff08;SpringBootPython爬虫实战&#xff09; ( 火…

cmake-find_package链接第三方库

文章目录 基本调用形式和模块模式使用方式 之前我们是使用了绝对路径来链接OpenCV第三方库&#xff0c;但是现在很多库一般会自己写一些cmake文件提供给用户&#xff0c;用户可以直接使用其中的内置变量即可。使用的命令就是find_package。 基本调用形式和模块模式 find_packa…

【RTP】webrtc 学习2: webrtc对h264的rtp打包

切片只是拷贝帧的split的各个部分到新的rtp 包的封装中。并没有在rtp包本身标记是否为关键帧FU-A 切片 输入的H.264 数据进行split :SplitNalu SplitNalu : 按照最大1200字节进行切分 切分后会返回一个数组 对于FU-A :split的数据总大小是 去掉一个字节的nalu header size …

CROSS JOIN

CROSS JOIN 是 SQL 中用于执行笛卡尔积&#xff08;Cartesian product&#xff09;的一种连接操作。它会将左表的每一行与右表的每一行进行组合&#xff0c;生成的结果集的行数等于左表的行数乘以右表的行数。 举个例子&#xff0c;如果表 A 有 m 行&#xff0c;表 B 有 n 行&…

qt的main函数(程序启动入口)

函数入口的参数 这就是Qt中最简单的一个main函数&#xff1a; int main(int argc, char *argv[]) {QApplication a(argc, argv);Widget w;w.show();return a.exec(); } 其中int argc, char *argv[]参数是很有用的。 使用.\release\程序名.exe 模型名.model 模型文件所在的地…

实战 | OpenCV+OCR实现弧形文字识别实例(详细步骤 + 源码)

导 读 本文主要介绍基于OpenCV+OCR实现弧形文字识别实例,并给详细步骤和代码。源码在文末。 背景介绍 测试图如下,目标是正确识别图中的字符。图片来源: https://www.51halcon.com/forum.php?mod=viewthread&tid=6712 同样,论坛中已经给出了Halcon实现代码,…

1948-2022年金融许可信息明细数据

1948-2022年金融许可信息明细数据 1、时间&#xff1a;1948-2022年 2、来源&#xff1a;银监会&#xff08;银监会许可证发布系统&#xff09; 3、指标&#xff1a;来源表、机构编码、机构名称、所属银行、机构类型、业务范围、机构住所、地理坐标、行政区划代码、所属区县、…

【计算机网络】深入掌握计算机网络的核心要点(面试专用)

写在前面 前言四层模型网络地址管理Linux下设置ipARP请求包总结 前言 计算机网络是指将分散的计算机设备通过通信线路连接起来&#xff0c;形成一个统一的网络。为了使得各个计算机之间能够相互通信&#xff0c;需要遵循一定的协议和规范。OSI参考模型和TCP/IP参考模型是计算机…

(南京观海微电子)——OLED驱动与调试

一、OLED DDIC分类 OLED DDIC的技术方向可以分为3类&#xff1a;带Ram【内存】的IC、Ram-less IC和TDDI【显示&触控集成的IC】 1、带Ram的OLED DDIC OLED DDIC有两个Ram&#xff0c;分别是Demura Ram和Display Ram。 1、带Ram的OLED DDIC 1-1&#xff09;Demura Ram&a…

STM32 简易智能家居嵌入式系统设计蓝图

声明 本文为物联网产品设计蓝图,不包括程序设计。 文章目录 声明前言一、项目需求1. 1 数据采集1.2 执行器控制1.3 人机交互1.4 功能1.5 场景联动1.6 数据分析二、项目评估2.1 软硬件2.1.1 硬件2.1.2 软件2.2 设备通讯方式及网络协议三、技术预研3.1 MQTT平台评估3.1.1 方案一…

一张图文深入了解信息量概念

通信原理第10页最后一段&#xff1a; 概率论告诉我们&#xff0c;事件的不确定程度可以用其出现的概率来描述。因此&#xff0c;消息中包含的信息量与消息发生的概率密切相关。消息出现的概率越小&#xff0c;则消息中包含的信息量就越大。 这句话怎么理解呢&#xff1f; 比如…

安利6款免费又高清的视频转GIF方法,值得收藏

前言 平时我们在聊天的时候会发的很多有趣表情包&#xff0c;其实有些就是视频里面的画面&#xff0c;觉得好玩有趣就被网友转换成了GIF&#xff0c;聊天的时候就可以用这些表情包来代表当时的心情。 如何将视频转成GIF动图&#xff1f;对于还不知道怎么将视频转成GIF的朋友&a…

【C语言】(8)宏定义

1. 简介 宏定义(#define)是C语言预处理指令的一种&#xff0c;用于为程序中的值或代码片段创建别名。宏定义可以使代码更加简洁、易于维护&#xff0c;并且可以提高代码重用性。 2. 基本用法 定义常量 宏可以用来定义常量&#xff0c;代替硬编码的数字或字符串&#xff0c;…

MyBatis --- 常用注解

目录 前言 1. Mapper 2. Select 3. Insert 4. Update 5. Delete 6. Results 注意事项&#xff1a; 前言 MyBatis是一款强大的持久层框架&#xff0c;通过注解的方式&#xff0c;可以更便捷地进行数据库操作。本文将介绍MyBatis中常用的注解以及在使用这些注解时需要注意…

uniapp微信小程序-秋云u-charts层级过高

一、先说问题 想在这个每个圆环上面定位一些百分比或者定位一些东西,微信小程序端可以&#xff0c;真机调试不行&#xff0c;打算提高层级不行 二、解决 点击底下开启2d就可以&#xff0c; 也就是在你的图表上加上 :canvas2d"true"

Vue 插槽讲解

什么是插槽&#xff1f; Slot 通俗的理解就是“占坑”&#xff0c;在组件模板中占好了位置&#xff0c;当使用该组件标签时候&#xff0c;组件标签里面的内容就会自动填坑&#xff08;替换组件模板中slot位置&#xff09;并且可以作为承载分发内容的出口。 简单的来说为了更加…

Adobe ColdFusion 任意文件读取漏洞复现(CVE-2023-26361)

0x01 产品简介 Adobe ColdFusion是美国奥多比(Adobe)公司的一套快速应用程序开发平台。该平台包括集成开发环境和脚本语言。 0x02 漏洞概述 Adobe ColdFusion平台 filemanager.cfc接口存在任意文件读取漏洞,攻击者可通过该漏洞读取系统重要文件(如数据库配置文件、系统配…

流畅的Python(六)-使用一等函数实现设计模式

一、核心要义 《设计模式&#xff1a;可复用面向对象软件的基础》一书中有23个设计模式&#xff0c;其中有16个在动态语言中"不见了或者简化了"。作为动态语言之一的Python, 我们可以利用一等函数简化其中的某些设计模式&#xff0c;本章主要介绍如何使用一等函数重…

c++ QT 信号的个人理解 信号就是独立文件调用的一种“协议”

一. 简介 就我个人来理解&#xff0c;信号槽机制与Windows下消息机制类似&#xff0c;消息机制是基于回调函数&#xff0c;Qt中用信号与槽来代替函数指针&#xff0c;使程序更安全简洁。 信号和槽机制是 Qt 的核心机制&#xff0c;可以让编程人员将互不相关的对象绑定在一起&a…

写一段防止sql注入的sql查询

要防止SQL注入攻击&#xff0c;可以使用参数化查询或预编译查询来防止恶意SQL代码的注入。下面是一个使用参数化查询的示例&#xff1a; DECLARE username nvarchar(50) ?; DECLARE password nvarchar(50) ?;SELECT * FROM users WHERE username username AND password …