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

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;
    // 锁定key
    String 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_'";

    @Autowired
    private 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未执行完方法之前,后面的请求都无法到达,封装好后,只需要一个注解就可以防止并发

第一个请求成功:
在这里插入图片描述
第二个请求失败:
在这里插入图片描述
第三个请求失败:
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

(电脑硬件)台式机主板音频端口功能详解

当你想给你的主机插上音响或者耳机时&#xff0c;你会发现主板上有6个接口&#xff0c;同样都是3.5mm接口&#xff0c;你知道该插哪个吗&#xff1f; 一般情况下&#xff0c;后置输入输出端口面板中&#xff0c;大多数的主板音频部分是彩色的。这一类颜色跟功能基本是固定的。当…

代码随想录训练营Day53| 1143.最长公共子序列 1035.不相交的线 53. 最大子序和 动态规划

目录 学习目标 学习内容 1143.最长公共子序列 1035.不相交的线 53. 最大子序和 动态规划 学习目标 1143.最长公共子序列 1035.不相交的线 53. 最大子序和 动态规划 学习内容 1143.最长公共子序列 1143. 最长公共子序列 - 力扣&#xff08;LeetCode&#xff09;ht…

通过ChatGPT跟MetaHuman对话,Android/iOS兼容

一、申请ChatGPT的API-KEY 1.通过 openAI官网申请API-KEY 2.参考使用腾讯云函数一分钟搭建 OpenAI 免翻墙代理搭建openAI免翻墙代理 3.通过Postman测试一下openAI函数是否可被调用,传入BearerToken和Body参数,ChatGPT即可返回应答数据 二、启用必要的插件 1.启用文字转语…

文本三剑客-Sed

sed工作原理 sed的特点&#xff1a; sed基本语法 模式空间中的编辑操作---地址定界 常用编辑命令 增添&#xff08;a&#xff09; 追加&#xff08;i&#xff09; 删除&#xff08;d&#xff09; 读入文件&#xff08;r&#xff09; 固定长度替换&#xff08;y&#xff0…

Segment Anything专题论文和代码汇总

文章目录 2023Scaling-up Remote Sensing Segmentation Dataset with Segment Anything ModelPersonalize Segment Anything Model with One ShotSegment Anything in Medical ImagesMatcher: Segment Anything with One Shot Using All-Purpose Feature MatchingCustomized Se…

程序员困局:去大城市进大厂却买不了房,回老家又没有高薪工作…

对于在外打拼的程序员来说&#xff0c;难的是进大厂&#xff0c;而不是买不起房。 进大厂的程序员&#xff0c;能不能买得起房&#xff1f; 进大厂的程序员的薪资&#xff0c;还是相当可观的。以阿里P6为例&#xff0c;年薪50万&#xff0c;到手40万左右&#xff0c;刨去10万…

C++小知识点(auto关键字)

&#x1f339;作者:云小逸 &#x1f4dd;个人主页:云小逸的主页 &#x1f4dd;Github:云小逸的Github &#x1f91f;motto:要敢于一个人默默的面对自己&#xff0c;强大自己才是核心。不要等到什么都没有了&#xff0c;才下定决心去做。种一颗树&#xff0c;最好的时间是十年前…

5月跳槽会有风险,不跳也会有?

今天讲讲跳槽。 说实话跳槽是为了寻求更好的发展&#xff0c;但在跳槽前我们也不能确定下家就是更好的归宿&#xff0c;这就更加需要我们审慎地去对待&#xff0c;不能盲目跳槽。 其次&#xff0c;我们离职和跳槽&#xff0c;其中的原因很大一部分是目前薪资不符合预期。 那…

【面试篇】SpringIoC、AOP、MVC面试实战

version&#xff1a;1.0 文章目录 SpringSpring基础 / IoC&#x1f64e;‍♂️面试官&#xff1a;举例Spring的模块&#xff1f;&#x1f64e;‍♂️面试官&#xff1a;Spring、SpringMVC、Spring Boot关系&#xff1f;&#x1f64e;‍♂️面试官&#xff1a;说说对SpringIoC的…

人工智能值不值得学习?人工智能就业方向及前景

人工智能值不值得学习? 一、人工智能值得学吗&#xff1f; 很多同学想要知道人工智能值得学吗&#xff1f;小编认为是值得的&#xff0c;具体原因有以下两点&#xff1a; 1、人工智能专业前景好&#xff0c;但人才紧缺 根据人工智能行业的专家预计&#xff0c;到2020年&am…

探索【Stable-Diffusion WEBUI】的图片超分辨插件:StableSR

文章目录 &#xff08;零&#xff09;前言&#xff08;一&#xff09;图片放大&#xff08;二&#xff09;图片超分辨率放大脚本插件&#xff08;StableSR&#xff09;&#xff08;2.1&#xff09;下载组件&#xff08;2.2&#xff09;使用&#xff08;2.3&#xff09;实例对比…

网上学影视后期靠谱吗 影视后期剪辑需要学什么

影视后期如果有人手把手当面教的话&#xff0c;当然是最好的。但很多人都没有这么好的条件&#xff0c;实际上&#xff0c;网上也有很多教程可以学习利用。不过&#xff0c;小伙伴们可能会有疑问&#xff0c;网上学影视后期靠谱吗&#xff0c;影视后期剪辑需要学什么&#xff1…

Java API 基础

Java API 基础 一、相关知识学习 Java程序员在开发Java程序时&#xff0c;只需要安装有JDK&#xff0c;就可以在程序中使用import关键字导入Java API 中指定的包并在自己的程序中使用这些包中定义的各种类和接口。 1、 Java API 包 Java API 包 说明 java.accessibility 接…

【服务器】支付宝SDK接口调试

​ 文章目录 1.测试环境2.本地配置3. 内网穿透3.1 下载安装cpolar内网穿透3.2 创建隧道 4. 测试公网访问5. 配置固定二级子域名5.1 保留一个二级子域名5.2 配置二级子域名 6. 使用固定二级子域名进行访问 转发自cpolar内网穿透的文章&#xff1a;Java支付宝沙箱环境支付&#…

物联网通信协议-MQTT及使用python实现

MQTT概念及其原理 简述 MQTT(Message Queuing Telemetry Transport&#xff0c;消息队列遥测传输协议&#xff09;&#xff0c;是一种基于发布/订阅&#xff08;publish/subscribe)模式的"轻量 级"通讯协议&#xff0c;该协议构建于TCP/IP协议上&#xff0c;由IBM在…

御剑WEB指纹识别系统教程,图文教程(超详细)

「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 御剑WEB指纹识别 一、基本使用二、内置字典三、自定义字典四、扫描原理 御剑WEB指纹识别系…

基于DDSRF正负序分离方法的不平衡电网PQ控制策略_平衡电流控制

0.前言 对于并网逆变器而言&#xff0c;电网会存在不平衡的情况。在这种情况下&#xff0c;不平衡的电网电压可以分解成为正序、负序和零序分量。并网逆变器通常期望能够实现单位功率因数并网&#xff0c;向电网注入对称的正弦电流&#xff0c;所以此时的微电网逆变器控制策略显…

rollup打包react组件

这次主要简单实现用rollup打包react组件&#xff0c;组件的话简单写了一个弹窗组件&#xff0c;效果如下&#xff1a; 点击打开弹框&#xff0c;点击关闭按钮关闭弹框 首先创建react项目&#xff0c;这边还是用mfex-project脚手架创建 mfex-project create react-demo 然后编…

财务共享领先实践,看看他们是怎么做的

随着信息技术的快速发展&#xff0c;由于创新商业模式的出现&#xff0c;金融结构、操作策略和流程正在发生变化。大数据、云计算、人工智能、机器人流程自动化&#xff08;RPA&#xff09;等新兴技术正在应用于金融及财务领域&#xff0c;以优化财务管理流程并提高运营效率。财…

信号完整性分析基础知识之传输线和反射(七):带负载传输线、感性不连续引起的反射

带负载传输线 如果在传输线上有一个小的容性负载&#xff0c;信号会出现失真&#xff0c;上升时间也会降低。每个分立电容都会降低信号在其附近看到的阻抗。如果传输线上分布有多个容性负载&#xff08;例如一个总线上每隔1.2inch有一个2pF的连接器残桩&#xff0c;或者一个内…