springboot实现后端防重复提交(AOP+redis分布式锁)单机情况下

文章目录

  • 0、依赖
  • 1、自定义接口
  • 2、实现redis分布式锁
  • 3、统一返回值ReturnT
  • 4、CookieUtil
  • 5、自定义AOP
  • 6、测试

为什么要实现这个功能呢,可能用户在提交一份数据后,可能因为网络的原因、处理数据的速度慢等原因导致页面没有及时将用户刚提交数据的后台处理结果展示给用户,这时用户可能会进行如下操作:

  1. 1秒内连续点击提交按钮,导致重复提交表单。
  2. 使用浏览器后退按钮重复之前的操作,导致重复提交表单
    数据库中会存在大量重复的信息。

怎么实现呢:进入正题👇

0、依赖

		<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.6.8</version></dependency><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-core</artifactId><version>9.0.21</version></dependency><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId><version>2.6.3</version></dependency>

1、自定义接口

/*** 自定义注解防止表单重复提交* @author Yuan Haozhe*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {/*** 防重复操作过期时间,默认1s*/long expireTime() default 1;
}

2、实现redis分布式锁

@Component
public class RedisLock {@Autowiredprivate StringRedisTemplate redisTemplate;/*** 该加锁方法仅针对单实例 Redis 可实现分布式加锁* 对于 Redis 集群则无法使用** 支持重复,线程安全** @param lockKey   加锁键* @param clientId  加锁客户端唯一标识(采用UUID)* @param seconds   锁过期时间* @return*/public boolean tryLock(String lockKey, String clientId, long seconds) {//return redisTemplate.opsForValue().setIfAbsent(lockKey, clientId, seconds, TimeUnit.SECONDS);}}

对参数进行介绍:
第一个为key,我们使用key来当锁,因为key是唯一的。我们传的是lockKey
第二个为value,通过给value赋值为clientId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。clientId可以使用UUID.randomUUID().toString()方法生成。
第三个为time,代表key的过期时间
第四个为time的单位
setIfAbsent 相当于NX,设置过期时间相当于EX

也可用Jedis实现:

  1. 添加依赖:
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version>
</dependency>
  1. 加锁代码
	private static final String LOCK_SUCCESS = "OK";private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "PX";private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";@Autowiredprivate Jedis jedis;public boolean tryLock(String lockKey, String clientId, long seconds) {String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);if (LOCK_SUCCESS.equals(result)) {return true;}return false;}/*** 与 tryLock 相对应,用作释放锁** @param lockKey* @param clientId* @return*/public boolean releaseLock(String lockKey, String clientId) {//这里使用Lua脚本的方式,尽量保证原子性。return jedis.eval(RELEASE_LOCK_SCRIPT,Collections.singletonList(lockKey),Collections.singletonList(clientId)).equals(1L);}

对参数进行介绍:
第一个为key,我们使用key来当锁,因为key是唯一的。我们传的是lockKey
第二个为value,通过给value赋值为clientId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。clientId可以使用UUID.randomUUID().toString()方法生成。
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,代表key的过期时间。

可以看到,我们的加锁就一行代码,保证了原子性,总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

3、统一返回值ReturnT

public class ReturnT<T> implements Serializable {public static final long serialVersionUID = 42L;public static final int SUCCESS_CODE = 200;public static final int FAIL_CODE = 500;public static final ReturnT<String> SUCCESS = new ReturnT<String>(null);public static final ReturnT<String> FAIL = new ReturnT<String>(FAIL_CODE, null);private int code;private String msg;private T data;public ReturnT(int code, String msg) {this.code = code;this.msg = msg;}public ReturnT(T data) {this.code = SUCCESS_CODE;this.data = data;}public int getCode() {return code;}public void setCode(int code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public T getData() {return data;}public void setData(T data) {this.data = data;}}

4、CookieUtil

public class CookieUtil {/*** 查询value** @param request* @param key* @return*/public static String getValue(HttpServletRequest request, String key) {Cookie cookie = get(request, key);if (cookie != null) {return cookie.getValue();}return null;}}

5、自定义AOP

@Component
@Aspect
@Slf4j
public class NoRepeatSubmitAspect {@Pointcut("@annotation(com.xxl.sso.base.annotation.RepeatSubmit)")public void repeatSubmit(){}@Autowiredprivate HttpServletRequest request;@Autowiredprivate RedisLock redisLock;@Around("repeatSubmit()")public ReturnT around(ProceedingJoinPoint joinPoint) {log.info("校验重复提交");//用户的身份标识,这里用cookie只是方便做测试String userId = CookieUtil.getValue(request, Conf.SSO_SESSIONID).split("_")[0];String key = getKey(userId, request.getServletPath());String clientId = getClientId();Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();// 获取防重复提交注解RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);boolean isSuccess = redisLock.tryLock(key, clientId, annotation.expireTime());// 如果缓存中有这个url视为重复提交if (isSuccess) {Object result = null;try {result = joinPoint.proceed();} catch (Throwable e) {log.error(e.getMessage());}//finall{//redisLock.releaseLock(key, clientId)//}return ReturnT.SUCCESS;} else {log.error("重复提交");return ReturnT.FAIL;}}private String getKey(String token, String path) {return token + path;}private String getClientId() {return UUID.randomUUID().toString();}
}

6、测试

 //测试重复提交@GetMapping("/test-get")//添加RepeatSubmit注解,默认1s,方便测试查看,写3s@RepeatSubmit(expireTime = 3)public ReturnT repeatTest(){return ReturnT.SUCCESS;}

结果:
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【经典题目分析】数组分割问题

文章目录 698. 划分为k个相等的子集416. 分割等和数组 698. 划分为k个相等的子集 把一个数组&#xff0c;拆分成K个大小一样的子数组。方法可以是状态枚举&#xff0c;或者dfs class Solution { public:bool canPartitionKSubsets(vector<int>& nums, int k) {// 从…

Gateway网关

网关的作用 对用户请求作身份认证、权限校验将用户请求路由到微服务&#xff0c;并实现负载均衡对用户请求作限流 引入依赖 <!--nacos服务注册发现依赖--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter…

pdf如何导出为图片?分享三个方法PDF转图片!

将PDF文件转换为图片是在许多场景下都非常有用的操作&#xff0c;不仅能够保留原始文档的内容&#xff0c;还方便在各种平台上共享和展示。在本文中&#xff0c;我们将介绍三种简便的方法&#xff0c;帮助您将PDF文件快速转换为图片格式。 方法一&#xff1a;使用记灵在线工具…

Linux环境搭建(三)— 搭建数据库服务器

linux &#xff08;ubuntu&#xff09;安装mysql 和环境配置 一、安装MySql二、配置环境三、外网访问四、重置密码五、卸载 写在前面&#xff1a; 本文默认你的Linux系统已经安装vim&#xff0c;yum等&#xff0c;如你使用的是一个全新的操作系统&#xff0c;移步上一篇开始配置…

webpack相关面试题

webpack面试题 1.webpack和vite区别2.如何优化webpack打包速度&#xff1f;3.说说webpack中常见的Plugin&#xff1f;解决了什么问题4.说说如何借助webpack来优化前端性能&#xff1f;如何优化JS代码压缩CSS代码压缩Html文件代码压缩文件大小压缩图片压缩Tree ShakingusedExpor…

rsync 远程同步

rsync 远程同步 一、概念 rsync&#xff08;Remote Sync&#xff0c;远程同步&#xff09; 是一个开源的快速备份工具&#xff0c;可以在不同主机之间镜像同步整个目录&#xff0c;支持增量备份&#xff0c;并保持链接和权限&#xff0c;且采用优化的同步算法&#xff0c;传输…

爬虫入门指南(7):使用Selenium和BeautifulSoup爬取豆瓣电影Top250实例讲解【爬虫小白必看】

文章目录 介绍技术要点SeleniumBeautifulSoupOpenpyxl 实现步骤&#xff1a;导入所需库设置网页URL和驱动路径创建 ChromeDriver 服务配置 ChromeDriver创建 Excel 文件爬取数据关闭浏览器保存 Excel 文件 完整代码导出的excel 效果图未完待续.... 介绍 在本篇博客中&#xff…

使用docker搭建mysql集群

一、技术架构 1、架构图 2、解说 mysql_1、mysql_2、mysql_3是一组主从模式,同理mysql_4、mysql_5、mysql_6也是一组主从模式从上面的图可以看出mysql_1和mysql_4是主节点,可以进行增删改查操作,但是子几点只能查询操作如果mysql_1节点出现问题了&#xff0c;有mysql_4节点组…

Android Binder通信原理(七):java 下的C-S

源码基于&#xff1a;Android R 0. 前言 在之前的几篇博文中&#xff0c;对Android binder 的通信原理进行的深入的剖析&#xff0c;这些博文包括&#xff1a;binder 简介、servicemanager启动、service注册、service获取、Java 端的service 注册和获取。 在前一文中&#xf…

五、卷积神经网络

文章目录 前言一、图像卷积1.1 不变性1.2 互相关运算1.3 卷积层1.4 互相关和卷积1.5 特征映射和感受野 二、填充和步幅2.1 填充2.2 步幅 三、多输入多输出通道3.1 多输入通道3.2 多输出通道3.3 11卷积层 四、汇聚层/池化层4.1 最大汇聚层与平均汇聚层4.2 填充和步幅4.3 多个通道…

Squid代理服务器

Squid代理服务器 一、Squid相关知识 1.功能 Squid 主要提供缓存加速、应用层过滤控制的功能。 2.工作机制 1&#xff0e;代替客户机向网站请求数据&#xff0c;从而可以隐藏用户的真实IP地址。 2&#xff0e;将获得的网页数据&#xff08;静态 Web 元素&#xff09;保存到…

4.Nginx缓存设置和CDN

文章目录 Nginx缓存设置设置缓存取消不需要内容的缓存查看nginx缓存数据 CDN概念工作原理 Nginx缓存设置 设置缓存 ##在yum配置文件中添加nginx在线源vim /etc/yum.repos.d/nginx.repo[nginx-stable] namenginx stable repo baseurlhttp://nginx.org/packages/centos/7/$base…