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

2024-03-09 15:52

本文主要是介绍这样用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攻击,造成服务器瘫痪

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

这篇关于这样用redission分布式锁才优雅-自定义redission分布式锁注解(含spel表达式)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/791163

相关文章

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

集中式版本控制与分布式版本控制——Git 学习笔记01

什么是版本控制 如果你用 Microsoft Word 写过东西,那你八成会有这样的经历: 想删除一段文字,又怕将来这段文字有用,怎么办呢?有一个办法,先把当前文件“另存为”一个文件,然后继续改,改到某个程度,再“另存为”一个文件。就这样改着、存着……最后你的 Word 文档变成了这样: 过了几天,你想找回被删除的文字,但是已经记不清保存在哪个文件了,只能挨个去找。真麻烦,眼睛都花了。看

Oracle type (自定义类型的使用)

oracle - type   type定义: oracle中自定义数据类型 oracle中有基本的数据类型,如number,varchar2,date,numeric,float....但有时候我们需要特殊的格式, 如将name定义为(firstname,lastname)的形式,我们想把这个作为一个表的一列看待,这时候就要我们自己定义一个数据类型 格式 :create or repla

开源分布式数据库中间件

转自:https://www.csdn.net/article/2015-07-16/2825228 MyCat:开源分布式数据库中间件 为什么需要MyCat? 虽然云计算时代,传统数据库存在着先天性的弊端,但是NoSQL数据库又无法将其替代。如果传统数据易于扩展,可切分,就可以避免单机(单库)的性能缺陷。 MyCat的目标就是:低成本地将现有的单机数据库和应用平滑迁移到“云”端

如何更优雅地对接第三方API

如何更优雅地对接第三方API 本文所有示例完整代码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/third 我们在日常开发过程中,有不少场景会对接第三方的API,例如第三方账号登录,第三方服务等等。第三方服务会提供API或者SDK,我依稀记得早些年Maven还没那么广泛使用,通常要对接第三方