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

相关文章

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

Java 线程池+分布式实现代码

《Java线程池+分布式实现代码》在Java开发中,池通过预先创建并管理一定数量的资源,避免频繁创建和销毁资源带来的性能开销,从而提高系统效率,:本文主要介绍Java线程池+分布式实现代码,需要... 目录1. 线程池1.1 自定义线程池实现1.1.1 线程池核心1.1.2 代码示例1.2 总结流程2. J

Python中的sort方法、sorted函数与lambda表达式及用法详解

《Python中的sort方法、sorted函数与lambda表达式及用法详解》文章对比了Python中list.sort()与sorted()函数的区别,指出sort()原地排序返回None,sor... 目录1. sort()方法1.1 sort()方法1.2 基本语法和参数A. reverse参数B.

Spring的基础事务注解@Transactional作用解读

《Spring的基础事务注解@Transactional作用解读》文章介绍了Spring框架中的事务管理,核心注解@Transactional用于声明事务,支持传播机制、隔离级别等配置,结合@Tran... 目录一、事务管理基础1.1 Spring事务的核心注解1.2 注解属性详解1.3 实现原理二、事务事

Java JDK Validation 注解解析与使用方法验证

《JavaJDKValidation注解解析与使用方法验证》JakartaValidation提供了一种声明式、标准化的方式来验证Java对象,与框架无关,可以方便地集成到各种Java应用中,... 目录核心概念1. 主要注解基本约束注解其他常用注解2. 核心接口使用方法1. 基本使用添加依赖 (Maven

C#中通过Response.Headers设置自定义参数的代码示例

《C#中通过Response.Headers设置自定义参数的代码示例》:本文主要介绍C#中通过Response.Headers设置自定义响应头的方法,涵盖基础添加、安全校验、生产实践及调试技巧,强... 目录一、基础设置方法1. 直接添加自定义头2. 批量设置模式二、高级配置技巧1. 安全校验机制2. 类型

SpringBoot AspectJ切面配合自定义注解实现权限校验的示例详解

《SpringBootAspectJ切面配合自定义注解实现权限校验的示例详解》本文章介绍了如何通过创建自定义的权限校验注解,配合AspectJ切面拦截注解实现权限校验,本文结合实例代码给大家介绍的非... 目录1. 创建权限校验注解2. 创建ASPectJ切面拦截注解校验权限3. 用法示例A. 参考文章本文

Vite 打包目录结构自定义配置小结

《Vite打包目录结构自定义配置小结》在Vite工程开发中,默认打包后的dist目录资源常集中在asset目录下,不利于资源管理,本文基于Rollup配置原理,本文就来介绍一下通过Vite配置自定义... 目录一、实现原理二、具体配置步骤1. 基础配置文件2. 配置说明(1)js 资源分离(2)非 JS 资

SpringBoot 获取请求参数的常用注解及用法

《SpringBoot获取请求参数的常用注解及用法》SpringBoot通过@RequestParam、@PathVariable等注解支持从HTTP请求中获取参数,涵盖查询、路径、请求体、头、C... 目录SpringBoot 提供了多种注解来方便地从 HTTP 请求中获取参数以下是主要的注解及其用法:1