这样用redission分布式锁才优雅-自定义redission分布式锁注解(含spel表达式)

废话后面说,先上干货。
最终的使用效果是这样的:

    /*** 这里只是一个简单的示例,实际业务中,可能需要根据订单号查询订单信息,然后进行发货操作* 仅仅是为了证明相同订单号不能够同时操作,但是在实际的业务场景中,每个订单只能发货一次*/// 这个注解(@RedisLock)就是主角,它是一个自定义的注解,用于实现分布式锁。被注解的方法会在执行时,先获取锁,然后执行方法体,最后释放锁。@RedisLock(lockName = "deliver_goods", key = "#payParam.orderNumbers")@PostMapping("/deliver-goods")public ServerResponseEntity<Void> deliverGoods(@RequestBody PayParam payParam) {// 休眠两秒,模拟发货操作try {log.info("开始发货,订单号:{}", payParam.getOrderNumbers());TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);}log.info("发货成功,订单号:{}", payParam.getOrderNumbers());return ServerResponseEntity.success();}
// 实体类
public class PayParam {/*** 订单号*/private String orderNumbers;/*** 支付方式*/private Integer payType;}

上面的方法是模拟订单支付成功,商品发货的场景。同一笔订单只能发货一次,但是在服务器多节点且高并发的场景下,有可能两个服务器节点同时查询到商品待发货的状态,进而导致重复发货。此时,就需要将发货这一个逻辑加锁,异步变同步,保证线程安全。但是传统的synchronized无法在集群部署的服务器发挥作用,此时我们就需要用到传说中的分布式锁。
上面的例子,并不是采用传统的分布式锁的写法,而是仅仅用了一个注解搞定,相当的优雅,下面是这个注解背后的实现:

开发环境是JDK8+,项目基于SpringBoot搭建,先导包如下:

            <redisson.version>3.25.2</redisson.version>………<!-- 使用redisson集成分布式锁等 --><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>${redisson.version}</version></dependency><!-- 引入这个是为了间接引入SPEL表达式所需要的工具包 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 自定义注解作切面必备依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>

配置文件中配置redis地址

spring:data:redis:host: 192.168.*.*port: 6379password: *****

注意,上面的redis配置是spring.data.redis,是对 spring.redis 的扩展和增强,提供了更多的 Redis 配置选项和功能,例如支持 Redis Sentinel 和 Redis Cluster 等模式。因此,在 Spring Boot 2.x 及以上的版本中,推荐使用 spring.data.redis 进行 Redis 相关的配置。但对于 Spring Boot 1.x 版本仍然可以使用 spring.redis 进行配置。

自定义注解

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;/*** 使用redis进行分布式锁*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {/*** redis锁 名字*/String lockName() default "";/*** redis锁 key 支持spel表达式*/String key() default "";/*** 过期秒数,默认为5毫秒** @return 轮询锁的时间*/int expire() default 5000;/*** 超时时间单位** @return 秒*/TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}

切面实现类

@Aspect
@Component
@Slf4j
public class RedisLockAspect {/*** redisson 客户端*/@Autowiredprivate RedissonClient redissonClient;/*** redis锁前缀*/private static final String REDISSON_LOCK_PREFIX = "redisson_lock:";/*** 针对注解redisLock进行环绕,切面实现方法** @param joinPoint 切点* @param redisLock 注解* @return 方法响应* @throws Throwable*/@Around("@annotation(redisLock)")public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {String spel = redisLock.key();String lockName = redisLock.lockName();RLock rLock = redissonClient.getLock(getRedisKey(joinPoint,lockName,spel));rLock.lock(redisLock.expire(),redisLock.timeUnit());Object result = null;try {//执行方法result = joinPoint.proceed();} finally {rLock.unlock();}return result;}/*** 将spel表达式转换为字符串* @param joinPoint 切点* @return redisKey*/private String getRedisKey(ProceedingJoinPoint joinPoint,String lockName,String spel) {Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;Method targetMethod = methodSignature.getMethod();Object target = joinPoint.getTarget();Object[] arguments = joinPoint.getArgs();return REDISSON_LOCK_PREFIX + lockName + StrUtil.COLON + parse(target,spel, targetMethod, arguments);}/*** 支持 #p0 参数索引的表达式解析* @param rootObject 根对象,method 所在的对象* @param spel 表达式* @param method ,目标方法* @param args 方法入参* @return 解析后的字符串*/public String parse(Object rootObject,String spel, Method method, Object[] args) {if (StrUtil.isBlank(spel)) {return StrUtil.EMPTY;}if (!spel.contains("#")) {return spel;}// 语法校验checkSpEL(spel);//获取被拦截方法参数名列表(使用Spring支持类库)StandardReflectionParameterNameDiscoverer standardReflectionParameterNameDiscoverer = new StandardReflectionParameterNameDiscoverer();String[] paraNameArr = standardReflectionParameterNameDiscoverer.getParameterNames(method);if (ArrayUtil.isEmpty(paraNameArr)) {return spel;}//使用SPEL进行key的解析ExpressionParser parser = new SpelExpressionParser();//SPEL上下文StandardEvaluationContext context = new MethodBasedEvaluationContext(rootObject,method,args,standardReflectionParameterNameDiscoverer);//把方法参数放入SPEL上下文中for (int i = 0; i < paraNameArr.length; i++) {context.setVariable(paraNameArr[i], args[i]);}return parser.parseExpression(spel).getValue(context, String.class);}/*** SpEL 表达式校验*/private void checkSpEL(String spEL) {try {ExpressionParser parser = new SpelExpressionParser();parser.parseExpression(spEL, new TemplateParserContext());} catch (Exception e) {log.error("spEL表达式解析异常", e);throw new LittleException("Invalid SpEL expression [" + spEL + "]");}}}

搞定,就是这么简约且优雅,让我们再回顾一下开头提出的那个订单支付成功发货的案例,@RedisLock(lockName = “deliver_goods”, key = “#payParam.orderNumbers”)
这个注解其中的参数key就是支持固定字符和SPEL表达式,这里的key的写法就是SPEL表达式,表示获取请求入参payParam对象的orderNumbers参数值。
为了测试效果和便于理解,写个测试类测试一下:

测试类

@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class NewTest {@Autowiredprivate OrderController orderController;@Testpublic void testDeliverGoods() {Runnable runnable = () -> {log.info("线程{}启动", Thread.currentThread().getName());PayParam payParam1 = new PayParam();payParam1.setOrderNumbers("1234");orderController.deliverGoods(payParam1);};new Thread(runnable).start(); // 线程1启动new Thread(runnable).start(); // 线程2启动// 主线程休眠3秒,等待线程执行完毕try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}}}

测试方法的主要思想就是同时开启两个线程,用同一个订单号同时请求发货方法,看看能不能实现异步变同步,执行结果如下:

2024-03-09T14:07:23.900+08:00 INFO 29796 — [ Thread-2] : 线程Thread-2启动
2024-03-09T14:07:23.900+08:00 INFO 29796 — [ Thread-3] : 线程Thread-3启动
2024-03-09T14:07:23.927+08:00 INFO 29796 — [ Thread-3] : 开始发货,订单号:1234
2024-03-09T14:07:25.934+08:00 INFO 29796 — [ Thread-3] : 发货成功,订单号:1234
2024-03-09T14:07:25.944+08:00 INFO 29796 — [ Thread-2] : 开始发货,订单号:1234
2024-03-09T14:07:27.952+08:00 INFO 29796 — [ Thread-2] : 发货成功,订单号:1234

通过执行日志我们发现,虽然两个线程几乎同时启动,但是两个线程确实是依次执行发货方法,尽管方法执行需要两秒,但是Thread-2还是等待Thread-3释放了锁之后才能够获取锁执行方法。

以上就是干货的全部内容,下面是一些闲谈。
上面的分布式锁能够发挥效果,主要是利用了redission的力量,在切面实现类中,我们调用了redission提供的lock方法:
rLock.lock(redisLock.expire(),redisLock.timeUnit());
这个方法是那我们设置的“key”去redis服务器设值,如果该key不存在于redis中则设置成功,程序继续运行;如果是redis中已有该“key”,则当前线程阻塞,等待该key释放并完成设值。
所以上述测试方法中Thread-2一直等待Thread-3释放了锁,才能继续执行。

想必很多同学已经发现了这个方法的风险,

  1. 如果Thread-3一直不释放锁获取占用锁时间过长,那么其他线程只能一直等待,造成资源浪费甚至死锁
  2. 如果有心之人发现你的方法存在阻塞,有可能利用这个进行DOS攻击,造成服务器瘫痪

下一篇我们进行一点小优化,规避以上风险

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

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

相关文章

Ubantu 18.04 如何映射IP到公网,外网可以访问

介绍一种简单的方式&#xff0c;就是通过路由侠 inux 系统安装路由侠&#xff0c;可通过两种方式进行&#xff0c;一种是通过直接脚本安装&#xff0c;一种是通过 Docker 安装。 windows下载地址&#xff1a;路由侠-局域网变公网 方式一&#xff1a;通过脚本安装 1、获取安…

java算法第十七天 | ● 110.平衡二叉树 ● 257. 二叉树的所有路径 ● 404.左叶子之和

110.平衡二叉树 leetcode链接 思路&#xff1a; 使用后序遍历分别求左右子树的高度&#xff0c;若高度只差大于一&#xff0c;则返回-1&#xff0c;否则返回当前节点的最大高度。 /*** Definition for a binary tree node.* public class TreeNode {* int val;* Tree…

【数据分享】2013-2022年全国范围逐日SO2栅格数据

空气质量数据是在我们日常研究中经常使用的数据&#xff01;之前我们给大家分享了2013-2022年全国范围逐月SO2栅格数据和逐年SO2栅格数据&#xff08;均可查看之前的文章获悉详情&#xff09;。 本次我们给大家带来的是2013-2022年全国范围的逐日的SO2栅格数据&#xff0c;原始…

阿里云几核服务器够用?内存多少合适?

阿里云服务器配置怎么选择&#xff1f;CPU内存、公网带宽和系统盘怎么选择&#xff1f;个人开发者或中小企业选择轻量应用服务器、ECS经济型e实例&#xff0c;企业用户选择ECS通用算力型u1云服务器、ECS计算型c7、通用型g7云服务器&#xff0c;阿里云服务器网aliyunfuwuqi.com整…

python基础总复习

Python基础班总复习 一、Python基础语法 1、注释概念 单行注释 # 多行注释 注释内容 &#xff0c;支持换行 > 在实际工作中&#xff0c;主要用于实现函数说明文档 2、变量的概念 场景&#xff1a;保存数据&#xff0c;所以理论上有数据的地方都有变量&#xff01; 变…

OJ_二叉排序树

题干 C实现 循环双指针法(一个指向父亲&#xff0c;一个指向待插入结点) #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <queue> using namespace std;struct TreeNode {char data;TreeNode* left;TreeNode* right; };void InsertBST(TreeNode* …

C# OpenCvSharp DNN FreeYOLO 人脸检测

目录 效果 模型信息 项目 代码 下载 C# OpenCvSharp DNN FreeYOLO 人脸检测 效果 模型信息 Inputs ------------------------- name&#xff1a;input tensor&#xff1a;Float[1, 3, 192, 320] --------------------------------------------------------------- Outp…

每日OJ题_链表⑤_力扣25. K 个一组翻转链表

目录 力扣25. K 个一组翻转链表 解析代码 力扣25. K 个一组翻转链表 25. K 个一组翻转链表 难度 困难 给你链表的头节点 head &#xff0c;每 k 个节点一组进行翻转&#xff0c;请你返回修改后的链表。 k 是一个正整数&#xff0c;它的值小于或等于链表的长度。如果节点总…

音视频按照时长分类小工具

应某用户的需求&#xff0c;编写了这款根据音视频时长分类小工具。 实际效果如下&#xff1a; 显示的是时分秒&#xff1a; 核心代码&#xff1a; MediaInfo MI; if (MI.Open(strPathInput.c_str()) 0){return -1;}_tstring stDuration MI.Get(stream_t::Stream_Audio,0,_T…

斐波那契数 爬楼梯 使用最小花费爬楼梯

509. 斐波那契数 力扣题目链接(opens new window) 斐波那契数&#xff0c;通常用 F(n) 表示&#xff0c;形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始&#xff0c;后面的每一项数字都是前面两项数字的和。也就是&#xff1a; F(0) 0&#xff0c;F(1) 1 F(n) F(n -…

GitHub和Gitee的基本使用和在IDEA中的集成

文章目录 【1】GitHub1.创建仓库2.增加和修改文件3.创建分支4.删除仓库5.远程仓库下载到本地 【2】Gitee1.创建仓库2.远程仓库下载到本地. 【3】IDEA集成GitHub【4】IDEA集成Gitee1.在Gitee中修改&#xff0c;同步到本地2.从Gitee中下载项目 【1】GitHub 1.创建仓库 先登陆这…

阿里云99计划优惠:云服务器租用价格61元、99元、165元

阿里云99计划还有谁不知道么&#xff1f;阿里云不杀熟&#xff0c;新老用户同享&#xff0c;阿里云服务器99元一年&#xff0c;续费也是99元&#xff0c;续费不涨价家人们&#xff0c;2024年阿里云把云服务器价格打下来了&#xff0c;2核2G、2核4G、4核8G、4核16G、8核16G、8核…

【无标题】带大家做一个,易上手的家常西芹牛肉丸

这里 我准备的是 潮汕手工牛肉丸 都是弄好 里面有盐的 先拿出来清水化冰 准备一些西芹 切小段 一根胡萝卜 萝卜切片 和西芹段装在一起 调一碗料汁 两勺胡椒粉 一勺淀粉 一点清水 然后拿勺子搅拌均匀 三瓣大蒜 切成蒜末 导入 西芹段 萝卜片 高过菜的清水 一小勺食用油 小…

Web APIs 4 日期对象、节点操作

Web APIs 4 一、日期对象实例化日期对象方法案例&#xff1a;页面显示时间 时间戳 二、节点操作查找结点①父节点查找②子节点查找③兄弟节点查找 增加节点克隆节点删除节点 三、M端事件四、JS插件 一、日期对象 学习路径&#xff1a;实例化、日期对象方法、时间戳 实例化 …

2022 年 3 月青少年软编等考 C 语言一级真题解析

目录 T1. 双精度浮点数的输入输出思路分析 T2. 足球联赛积分思路分析 T3. 小写字母的判断思路分析 T4. 足球联赛积分 2思路分析 T5. 与 7 无关的数思路分析 T1. 双精度浮点数的输入输出 输入一个双精度浮点数&#xff0c;保留 8 8 8 位小数&#xff0c;输出这个浮点数。 时间…

【MySQL 系列】MySQL 语句篇_DML 语句

DML&#xff08;Data Manipulation Language&#xff09;&#xff0c;即数据操作语言&#xff0c;用于操作数据库对象中所包含的数据。常用关键字包括&#xff1a;插入&#xff08;INSERT&#xff09;、更新&#xff08;UPDATE&#xff09;、删除&#xff08;DELETE&#xff09…

欧拉计划第4题:Largest palindrome product(枚举 回文数)

Problem 4&#xff1a;Largest palindrome product 标签&#xff1a;枚举、回文数 原文&#xff1a;A palindromic number reads the same both ways. The largest palindrome made from the product of two 2 2 2-digit numbers is 9009 91 99 9009 91 \times 99 90099…

Linux命令详解——mkdir创建文件夹与touch创建文件

在windows图形化系统中想要通识创建多个文件夹似乎是一件比较困难的事情&#xff0c;但在linux系统下&#xff0c;这将变得简单 mkdir参数&#xff0c;-p&#xff0c;递归创建文件夹 mkdir创建多个文件 touch可以创建文件&#xff0c;以及修改文件时间

idea远程服务器debug

前提 本地代码和服务器代码一致 idea中创建远程服务 一般只需要修改ip&#xff0c;注意这边的端口是监听Socket的端口&#xff0c;不是服务的端口 然后把运行参数复制一下 -agentlib:jdwptransportdt_socket,servery,suspendn,address5005 tomcat启动 在tomcat的lib下的c…

Pytorch学习 day07(神经网络基本骨架的搭建、2D卷积操作、2D卷积层)

神经网络基本骨架的搭建 Module&#xff1a;给所有的神经网络提供一个基本的骨架&#xff0c;所有神经网络都需要继承Module&#xff0c;并定义_ _ init _ _方法、 forward() 方法在_ _ init _ _方法中定义&#xff0c;卷积层的具体变换&#xff0c;在forward() 方法中定义&am…