基于Redis的Java分布式锁,接口并发处理,并发方案

news/2024/11/17 6:21:08/

Redis的分布式锁很多人都知道,比如使用Jedis的setNx、incr等方法都可以实现分布式锁的功能,但是Jedis需要自己管理连接池,就稍微麻烦一点。
今天介绍的是使用RedisTemplate+切面编程+自定义注解+SPEL来实现分布式锁的功能,封装完成后只需要一个注解就可以解决分布式锁的问题,而且开箱即用,对业务代码完全没有侵入。

一、新建一个springBoot项目

代码结构如下:
在这里插入图片描述

二、编写代码

1、创建自定义注解ConcurrentLock

import java.lang.annotation.*;/*** @author wangyi* @date 2023-05-11* @description 分布式锁,防止重复提交*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConcurrentLock {// 锁定时间,单位秒long lockTime() default 5;// 锁定keyString lockKey();
}

2、封装SPEL表达式解析工具类SpELParser

主要用于解析自定义注解ConcurrentLock 的lockKey

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;import java.lang.reflect.Method;/*** @author wangyi* @date 2023-05-11* @description 解析spel表达式工具类*/
public class SpELParser {private EvaluationContext context;private ExpressionParser parser;private LocalVariableTableParameterNameDiscoverer discoverer;public SpELParser(JoinPoint jp) throws Exception {discoverer = new LocalVariableTableParameterNameDiscoverer();parser = new SpelExpressionParser();getContext(jp);}public SpELParser(ProceedingJoinPoint pjp) throws Exception {discoverer = new LocalVariableTableParameterNameDiscoverer();parser = new SpelExpressionParser();getContext(pjp);}public <T> T parseExpression(String expression, Class<T> clazz) {return parser.parseExpression(expression).getValue(context, clazz);}private void getContext(JoinPoint jp) throws Exception {Object[] args = jp.getArgs();Method method = ((MethodSignature) jp.getSignature()).getMethod();getContext(method, args);}private void getContext(ProceedingJoinPoint pjp) throws Exception {Object[] args = pjp.getArgs();Method method = ((MethodSignature) pjp.getSignature()).getMethod();getContext(method, args);}private void getContext(Method method, Object[] args) throws Exception {context = new StandardEvaluationContext();String[] names = discoverer.getParameterNames(method);for (int i = 0; i < args.length; i++) {context.setVariable(names[i], args[i]);}}
}

3、创建切面类ConcurrentLockAspect

分布式锁的具体逻辑封装在这个类,使用的是redisTemplate的setIfAbsent方法,如果不存在就设置,也是原子性操作,使用redisTemplate的好处是redisTemplate会自己管理连接池,但是方法没有Jedis多

import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/*** @author wangyi* @date 2023-05-11* @description 分布式锁实现*/
@Aspect
@Component
public class ConcurrentLockAspect {private static final String LOCK_VALUE = "1";// 测试Key前缀,需要使用''public static final String TEST_KEY = "'test_submit_'";@Autowiredprivate RedisTemplate redisTemplate;@Around("@annotation(concurrentLock)")public Object around(ProceedingJoinPoint joinPoint, ConcurrentLock concurrentLock) throws Throwable {if(StringUtils.isBlank(concurrentLock.lockKey())) {return null;}Object result = null;// 方法执行返回值try {// 获取到注解中的参数SpELParser spELParser = new SpELParser(joinPoint);String lockKey = spELParser.parseExpression(concurrentLock.lockKey(), String.class);// 如果解析出来key为空,直接执行目标方法if(StringUtils.isBlank(lockKey)) {result = joinPoint.proceed();} else {long lockTime = concurrentLock.lockTime();// 加锁并设置过期时间Boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey, LOCK_VALUE, lockTime, TimeUnit.SECONDS);if(lockResult) {// 加锁成功,执行目标方法result = joinPoint.proceed();// 解锁redisTemplate.delete(lockKey);} else {// 并发加锁失败,抛出异常throw new RuntimeException("请求处理中,请勿重复提交");}}} catch (Exception e) {throw e;}return result;}
}

4、创建测试接口TestController

在需要防止并发的接口加上@ConcurrentLock(lockKey = ConcurrentLockAspect.TEST_KEY + " + #dto.id", lockTime = 10L)注解即可,lockKey是使用的SPEL表达式解析,要遵守SPEL表达式的规则,lockTime为最长锁定时间,

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;/*** @author wangyi* @date 2023-05-11* @description 测试接口*/
@RestController
@RequestMapping(value = "/lockDemo", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
public class TestController {/*** 分布式锁测试,指定Key防止并发* @param dto* @return*/@RequestMapping(value =  "/testLock1.action",method = RequestMethod.POST)@ConcurrentLock(lockKey = ConcurrentLockAspect.TEST_KEY + " + #dto.id", lockTime = 10L)public TestDTO testLock1(@RequestBody TestDTO dto) throws Exception {Thread.sleep(5000);//模拟业务逻辑处理return dto;}/*** 分布式锁测试,判断如果dto.id不为null传指定的key进去,为null就传‘’进去,SPEL表达式可以进行计算,逻辑判断都可以* @param dto* @return*/@RequestMapping(value =  "/testLock2.action",method = RequestMethod.POST)@ConcurrentLock(lockKey = "#dto.id != null ? "+ConcurrentLockAspect.TEST_KEY + " + #dto.id" + ":''", lockTime = 10L)public TestDTO testLock2(@RequestBody TestDTO dto) throws Exception {Thread.sleep(5000);//模拟业务逻辑处理return dto;}
}

测试对象TestDTO

/*** @author wangyi* @date 2023-05-11* @description*/
public class TestDTO {private String id;private String name;private String mobile;public String getId() {return id;}public void setId(String id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getMobile() {return mobile;}public void setMobile(String mobile) {this.mobile = mobile;}
}

引入redis的pom文件

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>

启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class LockDemoApplication {public static void main(String[] args) {SpringApplication.run(LockDemoApplication.class, args);}}

5、启动redis

在这里插入图片描述

6、配置application.properties

###Tomcat
server.port=8080
server.servlet.context-path=/spring.jackson.time-zone= GMT+8
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss###
spring.http.encoding.force=true
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
server.tomcat.uri-encoding=UTF-8
server.tomcat.max-http-post-size=-1
spring.servlet.multipart.max-file-size=50MB# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=

三、启动项目测试并发效果

同时请求接口,只有第一个接口访问成功,另外两个接口请求失败,因为接口睡眠了5秒,模拟业务逻辑处理,第一个接口请求进入接口之后,注解上定义的lockKey只要有相同key请求进去,在前一个相同lockKey未执行完方法之前,后面的请求都无法到达,封装好后,只需要一个注解就可以防止并发
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


http://www.ppmy.cn/news/89578.html

相关文章

低代码平台活字格,让我们一起感受低代码平台活字格的魅力

一份耕耘&#xff0c;一份收获&#xff0c;一段工作经历&#xff0c;让我认识了活字格。感觉活字格绝对是同类产品中的佼佼者。简单的拖拉拽&#xff0c;就实现一个完美的WEB页面,并且可做到前后端分离与交互。有了他&#xff0c;不擅长前端的我&#xff0c;也能大显身手了。告…

【dfs和bfs时间复杂度超时了,怎么优化呢?双向优化】【双向dfs——送礼物】【双向bfs——字串变换】

预习 笔记 复习 做题 专注 效率 记忆 欢迎观看我的博客&#xff0c;如有问题交流&#xff0c;欢迎评论区留言&#xff0c;一定尽快回复&#xff01;&#xff08;大家可以去看我的专栏&#xff0c;是所有文章的目录&#xff09;   文章字体风格&#xff1a; 红色文字表示&#…

六种基本网络拓扑结构详解

目录 1、总线型网络拓扑结构 2、星型网络拓扑结构 3、环形网络拓扑结构 4、树型网络拓扑结构 5、网状网络拓扑结构 6、混合网络型拓扑结构 常见的网络拓扑结构有以下6种&#xff1a;1.总线型网络拓扑结构&#xff1b;2.星型网络拓扑结构&#xff1b;3.环形网络拓扑结构&a…

chrome插件打包之后,显示此扩展程序可能已损坏

每日鸡汤&#xff0c;每个你想要学习的瞬间都是未来的你向自己求救 问题是这样的&#xff0c;我们有一个chrome插件的项目&#xff0c;但是我也没有参与开发&#xff0c;可以说此前对chrome插件一窍不通。但是今天呢&#xff0c;有个bug&#xff0c;要我改&#xff0c;我就拉一…

ASEMI代理韩景元可控硅C106M参数,C106M封装,C106M尺寸

编辑-Z 韩景元可控硅C106M参数&#xff1a; 型号&#xff1a;C106M 断态重复峰值电压VDRM&#xff1a;600V 通态电流IT(RMS)&#xff1a;4A 通态浪涌电流ITSM&#xff1a;30A 平均栅极功耗PG(AV)&#xff1a;0.2W 峰值门功率耗散PGM&#xff1a;1W 工作接点温度Tj&…

API对接是什么意思,技术分享

在计算机科学中&#xff0c;应用程序接口&#xff08;API&#xff09;是一种程序编程接口&#xff0c;定义了应用程序之间或应用程序和操作系统之间的通信方式。API对接就是在不同的应用程序之间实现数据交换和信息传输的过程。当两个不同的应用程序需要共享数据时&#xff0c;…

2023年21个最佳的Ruby测试框架

作者 | Veethee Dixit 测试人员总是在寻找最好的自动化测试框架&#xff0c;它能提供丰富的功能&#xff0c;并且语法简单、兼容性好、执行速度快。如果你选择将Ruby与Selenium结合起来进行web测试&#xff0c;那么可能需要搜索基于Ruby的测试框架进行web应用程序测试。 Ruby…

ChatGPT 基础使用方法

文章目录 1. ChatGPT 是下一代搜索引擎2. ChatGPT 是学习助手3. ChatGPT API 简介4. ChatGPT API 身份5. 开发痛点6. 机会与前景7. Images8. Audio 1. ChatGPT 是下一代搜索引擎 根据 3 月份对 ChatGPT 的使用&#xff0c;我对它的理解是下一代的搜索引擎&#xff0c;即能够根…