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

news2024/11/30 2:46:51

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未执行完方法之前,后面的请求都无法到达,封装好后,只需要一个注解就可以防止并发
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Cloud 容错机试 Hystrix 服务降级 RestTemplate:

Ribon的服务降级操作 雪崩效应&#xff1a; 如果短信服务炸了后面的所有服务就会起连锁反应造成全部服务挂掉&#xff0c;这就是雪崩效应&#xff0c;那么其实短信服务又不是我们主要业务&#xff0c;这个时候我们可以采用服务降级&#xff0c;服务降级就是暂时的把短信服务停…

java学习——ArrayList和泛型(学习记录)

学习资料来自菜鸟教程 ArrayList 类是一个可以动态修改的数组&#xff0c;与普通数组的区别就是它是没有固定大小的限制&#xff0c;我们可以添加或删除元素。 ArrayList 继承了 AbstractList &#xff0c;并实现了 List 接口。 ArrayList 类位于 java.util 包中&#xff0c;使…

事件驱动模型IO模型

什么是事件驱动模型&#xff1f; 事件驱动模型是一种计算机编程模型&#xff0c;它通过等待事件的触发&#xff0c;在事件被触发时执行对应的处理函数。这种模型下&#xff0c;程序不再按照严格的顺序执行命令&#xff0c;而是以事件为驱动进行执行。事件驱动模型更适合处理大…

[RSA议题分析] Finding Vulnerabilities through Static Analysis and Scripting

文章目录 简介议题分析发现漏洞 - 什么时候/为什么什么是漏洞挖掘漏洞价值 如何挖洞逆向工程环境从哪开始挑战 总结 简介 作者讲了挖漏洞的目标&#xff0c;和一些常用的挖漏洞的方法和如果你像现在开始挖掘二进制漏洞&#xff0c;那么你可以从memcpy开始。除此之外&#xff0…

【SpringBoot系列】Spring EL表达式的简介和快速入门

介绍 Sping EL&#xff08;Spring Expression Language 简称 SpEL&#xff09;是一种强大的表达式语言&#xff0c;支持在运行时查询和操作对象&#xff0c;它可以与 XML 或基于注解的 Spring 配置一起使用。语言语法类似于统一 EL&#xff0c;但提供了额外的功能,方法调用和字…

springboot+vue藏区特产销售平台(java项目源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的藏区特产销售平台。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 &#x1f495;&#x1f495;作者&#xff1a;风…

产品经理如何使用集简云实现工作流程自动化?

场景描述 作为一名产品经理&#xff0c;需要每天面对来自各个部门的需求&#xff0c;平时的工作内容更是复杂繁琐&#xff0c;画流程图、产品开发周期、产品描述、设计团队协作&#xff0c;新产品培训会、用户需求会&#xff0c;或跟进 bug 修复等等等等…… 对内不仅要参与业…

STM32与ESP32下载器设计

文章目录 背景STM32下载器使用现成的DAPlink选择自制DAPlink ESP32/ESP8266下载器连接接口STM32接口ESP32接口 背景 我们常用的单片机主要有STM32和ESP32&#xff0c;其中STM32下载要求SWD下载接口&#xff0c;ESP32下载要求串口&#xff0c;但需要控制ESP32 IO0和EN口高低电平…

前端人必须掌握的抓包技能(原理到实践)

目录 1. 前言 2. 抓包的原理 2.1 什么是抓包&#xff1f; 2.2 HTTP/HTTPS 抓包原理 2.2.1 HTTP 抓包原理 2.2.2 HTTPS 抓包原理 2.3 电脑如何抓手机的包 3. 抓包工具 whistle 3.1 whistle 是什么 如何快速使用 whistle 3.2 whistle 可以做的事情 4. whistle 实战案…

HP打印机网络连接扫描仪失败

财务反映,使用主机上的HP LaserJet Pro M329-HP Scan连接扫描仪提示失败。 测试果然失败,提示如下图: 点击修复后,提示 需要安装HP Print and Scan Doctor。 同意安装,并启动HP Print and Scan Doctor 点击开始,其开始搜索,过程有点慢。 第一次发现其连接错误,居然…

液晶显示常用概念

文章目录 数字液晶显示消隐区水平&#xff08;行&#xff09;消隐 HBlank垂直&#xff08;场&#xff09;消隐 VBlank RGB格式RGB555RGB565RGB888 VGA驱动原理时钟信号像素时钟同步信号DE信号&#xff08;有效数据选通信号&#xff09;DE信号与其他信号的关系 数字液晶显示消隐…

golang webhook源码和案例配合gitee实践

下载golang webhook源码和案例&#xff1a; https://download.csdn.net/download/qq_32421489/87824180 解压后go mod tidy下载依赖包 修改打包环境为Linux&#xff1a; go env -w GOOSlinux 打包命令&#xff1a;go build 打包后的可运行程序上传服务器后&#xff1a;参…

用本地连接集群进行压力测试,让你的测试更快更有效!

目录 引言 背景 详细步骤 1、首先打开终端 2、安装kubectl 3、配置kubeconfig 4.准备本地仓库文件 5.启动集群执行脚本 总结 引言 测试是软件开发中至关重要的一环&#xff0c;但长时间的等待和低效率的测试却常常让人感到烦躁。现在&#xff0c;我们推出了全新的解决…

真的裂开了呀,现在的00后,真是卷死了

谁说00后躺平了&#xff0c;但是有一说一&#xff0c;该卷的还是卷。这不&#xff0c;上个月我们公司来了个00后&#xff0c;工作没两年&#xff0c;跳槽到我们公司起薪22K&#xff0c;都快接近我了。 后来才知道人家是个卷王&#xff0c;从早干到晚就差搬张床到工位睡觉了。 …

7个简单步骤创建企业邮箱教程

创建企业邮箱电子邮件地址有几种不同的方法&#xff0c;可以使用电子邮件服务提供商&#xff0c;也可以使用电子邮件托管服务。由于Zoho Mail是世界上最受欢迎的电子邮件服务提供商之一&#xff0c;提供非常简单的帐户设置、大量功能和有竞争力的价格&#xff0c;我们将以它为例…

【敲敲云】免费的零代码产品 — 应用创建与设置

敲敲云可以创建不同的应用&#xff0c;每一个应用我们可以看做一个系统&#xff0c;例如销售系统、财务系统等等。下面我们来看看如何创建应用吧。 应用的基础操作&#xff1a; 应用的基础操作包含创建应用、修改应用、退出/删除应用、排序应用、维护应用、应用回收站 1、新建…

提前进入行业顶尖阵营:高性能计算实习的竞争优势

如今就业越来越内卷&#xff0c;尤其是计算机行业更是如此。加上GPT以及大模型的加持&#xff0c;各大企业纷纷降本增效&#xff0c;普通程序员逐渐失去竞争力。想要在竞争激烈的就业市场中脱颖而出&#xff0c;提前进入行业顶尖阵营是一个明智的选择。而高性能计算实习将为您提…

js中获取对象属性值(对象.属性和对象[属性]的区别)

一、在JavaScript中可以通过两种方式访问对象属性 (1)可以使用“ . ”来访问对象的属性。 (2)可以使用“ [ ] ”来访问对象的属性。 二、对象.属性和对象[属性]的区别&#xff1a; 1.相同点&#xff1a; &#xff08;1&#xff09;都可以获取对象的属性值&#xff1b; 2.不同…

C Primer Plus第七章编程练习答案

学完C语言之后&#xff0c;我就去阅读《C Primer Plus》这本经典的C语言书籍&#xff0c;对每一章的编程练习题都做了相关的解答&#xff0c;仅仅代表着我个人的解答思路&#xff0c;如有错误&#xff0c;请各位大佬帮忙点出&#xff01; 1.编写一个程序读取输入&#xff0c;读…

PID算法在流量控制中的应用

目录 增量式或位置式 目录 增量式或位置式 PID控制周期 T1 时间 T2 约4ms PID C代码 最近有小伙伴向我提问关于PID的问题&#xff1a;通过比例阀控制水流速度&#xff08;流量&#xff09;&#xff0c; 使用增量式还是位置式 PID&#xff1f;他的比例法驱动频率是500Hz…