这样用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

相关文章

Java实现自定义table宽高的示例代码

《Java实现自定义table宽高的示例代码》在桌面应用、管理系统乃至报表工具中,表格(JTable)作为最常用的数据展示组件,不仅承载对数据的增删改查,还需要配合布局与视觉需求,而JavaSwing... 目录一、项目背景详细介绍二、项目需求详细介绍三、相关技术详细介绍四、实现思路详细介绍五、完整实现代码

一文详解Java Stream的sorted自定义排序

《一文详解JavaStream的sorted自定义排序》Javastream中的sorted方法是用于对流中的元素进行排序的方法,它可以接受一个comparator参数,用于指定排序规则,sorte... 目录一、sorted 操作的基础原理二、自定义排序的实现方式1. Comparator 接口的 Lam

Spring如何使用注解@DependsOn控制Bean加载顺序

《Spring如何使用注解@DependsOn控制Bean加载顺序》:本文主要介绍Spring如何使用注解@DependsOn控制Bean加载顺序,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录1.javascript 前言2. 代码实现总结1. 前言默认情况下,Spring加载Bean的顺

Spring @Scheduled注解及工作原理

《Spring@Scheduled注解及工作原理》Spring的@Scheduled注解用于标记定时任务,无需额外库,需配置@EnableScheduling,设置fixedRate、fixedDe... 目录1.@Scheduled注解定义2.配置 @Scheduled2.1 开启定时任务支持2.2 创建

mapstruct中的@Mapper注解的基本用法

《mapstruct中的@Mapper注解的基本用法》在MapStruct中,@Mapper注解是核心注解之一,用于标记一个接口或抽象类为MapStruct的映射器(Mapper),本文给大家介绍ma... 目录1. 基本用法2. 常用属性3. 高级用法4. 注意事项5. 总结6. 编译异常处理在MapSt

如何自定义一个log适配器starter

《如何自定义一个log适配器starter》:本文主要介绍如何自定义一个log适配器starter的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录需求Starter 项目目录结构pom.XML 配置LogInitializer实现MDCInterceptor

Java Lambda表达式的使用详解

《JavaLambda表达式的使用详解》:本文主要介绍JavaLambda表达式的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、前言二、Lambda表达式概述1. 什么是Lambda表达式?三、Lambda表达式的语法规则1. 无参数的Lambda表

Spring @RequestMapping 注解及使用技巧详解

《Spring@RequestMapping注解及使用技巧详解》@RequestMapping是SpringMVC中定义请求映射规则的核心注解,用于将HTTP请求映射到Controller处理方法... 目录一、核心作用二、关键参数说明三、快捷组合注解四、动态路径参数(@PathVariable)五、匹配请

SpringCloud中的@FeignClient注解使用详解

《SpringCloud中的@FeignClient注解使用详解》在SpringCloud中使用Feign进行服务间的调用时,通常会使用@FeignClient注解来标记Feign客户端接口,这篇文章... 在Spring Cloud中使用Feign进行服务间的调用时,通常会使用@FeignClient注解

Druid连接池实现自定义数据库密码加解密功能

《Druid连接池实现自定义数据库密码加解密功能》在现代应用开发中,数据安全是至关重要的,本文将介绍如何在​​Druid​​连接池中实现自定义的数据库密码加解密功能,有需要的小伙伴可以参考一下... 目录1. 环境准备2. 密码加密算法的选择3. 自定义 ​​DruidDataSource​​ 的密码解密3