目录

一、哪些因素会引起重复提交?

二、重复提交会带来哪些问题?

三、订单的防重复提交你能想到几种方案?

四、自定义注解方式

4.1Java核心知识-自定义注解(先了解下什么是自定义注解)

4.1.1 Annotation(注解)

4.1.2什么是元注解

4.1.3java内置4种元注解

4.2AOP+自定义注解接口防重提交多场景设计

4.3代码实战防重提交自定义注解之Token令牌/参数方式

4.3.1 自定义注解token令牌方式

4.3.2 再看下参数的防重方式


一、哪些因素会引起重复提交?

开发的项目中可能会出现下面这些情况:
    前端下单按钮重复点击导致订单创建多次
    网速等原因造成页面卡顿,用户重复刷新提交请求
    黑客或恶意用户使用postman等http工具重复恶意提交表单

二、重复提交会带来哪些问题?

重复提交带来的问题
    会导致表单重复提交,造成数据重复或者错乱
    核心接口的请求增加,消耗服务器负载,严重甚至会造成服务器宕机

三、订单的防重复提交你能想到几种方案?

核心接口需要做防重提交,你能想到几种方式:
方式一:前端JS控制点击次数,屏蔽点击按钮无法点击
    前端可以被绕过,前端有限制,后端也需要有限制 
方式二:数据库或者其他存储增加唯一索引约束
    需要想出满足业务需求的唯一索引约束,比如注册的手机号唯一。但是有些业务是没有唯一性限制的,且重复提交也会导致数据错乱,比如你在电商平台可以买一部手机,也可以买两部手机
方式三:服务端token令牌方式
    下单前先获取令牌-存储redis 下单时一并把token提交并检验和删除-lua脚本
    分布式情况下,采用Lua脚本进行操作(保障原子性)

其中方式三 是大家采用的最多的,那有没更加优雅的方式呢?
    假如系统中不止一个地方,需要用到这种防重复提交,每一次都要写这种lua脚本,代码耦合性太强,这种又不属于业务逻辑,所以不推荐耦合进service中,可读性较低
    本博客采用自定义注解,达到更优雅的目的

四、自定义注解方式

4.1Java核心知识-自定义注解(先了解下什么是自定义注解)

 4.1.1 Annotation(注解)

从JDK 1.5开始, Java增加了对元数据(MetaData)的支持,也就是 Annotation(注解)。
    注解其实就是代码里的特殊标记,它用于替代配置文件
    常见的很多 @Override、@Deprecated等

4.1.2什么是元注解

元注解是注解的注解,比如当我们需要自定义注解时会需要一些元注解(meta-annotation),如@Target和@Retention

4.1.3java内置4种元注解

@Target 表示该注解用于什么地方
        ElementType.CONSTRUCTOR 用在构造器
        ElementType.FIELD 用于描述域-属性上
        ElementType.METHOD 用在方法上
        ElementType.TYPE 用在类或接口上
        ElementType.PACKAGE 用于描述包


    @Retention 表示在什么级别保存该注解信息
        RetentionPolicy.SOURCE 保留到源码上
        RetentionPolicy.CLASS 保留到字节码上
        RetentionPolicy.RUNTIME 保留到虚拟机运行时(最多,可通过反射获取)


    @Documented  将此注解包含在 javadoc 中
    @Inherited 是否允许子类继承父类中的注解
    @interface
        用来声明一个注解,可以通过default来声明参数的默认值
        自定义注解时,自动继承了java.lang.annotation.Annotation接口
        通过反射可以获取自定义注解

4.2AOP+自定义注解接口防重提交多场景设计

防重提交方式
    token令牌方式
    ip+类+方法方式(方法参数)
利用AOP来实现
    Aspect Oriented Program 面向切面编程, 在不改变原有逻辑上增加额外的功能
    AOP思想把功能分两个部分,分离系统中的各种关注点
    好处
        减少代码侵入,解耦
        可以统一处理横切逻辑
        方便添加和删除横切逻辑
业务流程:

4.3代码实战防重提交自定义注解之Token令牌/参数方式

4.3.1 自定义注解token令牌方式

第一步 自定义注解

import java.lang.annotation.*;
/*** 自定义防重提交*/
@Documented
@Target(ElementType.METHOD)//可以用在方法上
@Retention(RetentionPolicy.RUNTIME)//保留到虚拟机运行时,可通过反射获取
public @interface RepeatSubmit {/*** 防重提交,支持两种,一个是方法参数,一个是令牌*/enum Type { PARAM, TOKEN }/*** 默认防重提交,是方法参数* @return*/Type limitType() default Type.PARAM;/*** 加锁过期时间,默认是5秒* @return*/long lockTime() default 5;
}

第二步 引入redis

#-------redis连接配置-------
spring.redis.client-type=jedis
spring.redis.host=120.79.xxx.xxx
spring.redis.password=123456
spring.redis.port=6379
spring.redis.jedis.pool.max-active=100
spring.redis.jedis.pool.max-idle=100
spring.redis.jedis.pool.min-idle=100
spring.redis.jedis.pool.max-wait=60000

第三步 下单前获取令牌用于防重提交

    @Autowiredprivate StringRedisTemplate redisTemplate;/*** 提交订单令牌的缓存key*/public static final String SUBMIT_ORDER_TOKEN_KEY = "order:submit:%s:%s";/*** 下单前获取令牌用于防重提交* @return*/@GetMapping("token")public JsonData getOrderToken(){//获取登录账户long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();//随机获取32位的数字+字母作为tokenString token = CommonUtil.getStringNumRandom(32);//key的组成String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY,accountNo,token);//令牌有效时间是30分钟redisTemplate.opsForValue().set(key, String.valueOf(Thread.currentThread().getId()),30,TimeUnit.MINUTES);return JsonData.buildSuccess(token);}/*** 获取随机长度的串** @param length* @return*/private static final String ALL_CHAR_NUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";public static String getStringNumRandom(int length) {//生成随机数字和字母,Random random = new Random();StringBuilder saltString = new StringBuilder(length);for (int i = 1; i <= length; ++i) {saltString.append(ALL_CHAR_NUM.charAt(random.nextInt(ALL_CHAR_NUM.length())));}return saltString.toString();}

第四步 定义切面类-开发解析器

根据type区分是使用token方式 还是参数方式

先看下token的方式


import lombok.extern.slf4j.Slf4j;
import net.wnn.annotation.RepeatSubmit;
import net.wnn.constant.RedisKey;
import net.wnn.enums.BizCodeEnum;
import net.wnn.exception.BizException;
import net.wnn.interceptor.LoginInterceptor;
import net.wnn.util.CommonUtil;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;/*** 定义一个切面类**/
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {@Autowiredprivate StringRedisTemplate redisTemplate;/*** 定义 @Pointcut注解表达式, 通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法* 在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点* 方式一:@annotation:当执行的方法上拥有指定的注解时生效(本博客采用这)* 方式二:execution:一般用于指定方法的执行*/@Pointcut("@annotation(repeatSubmit)")public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {}/*** 环绕通知, 围绕着方法执行* @param joinPoint* @param repeatSubmit* @return* @throws Throwable* @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。* <p>* 方式一:单用 @Around("execution(* net.wnn.controller.*.*(..))")可以* 方式二:用@Pointcut和@Around联合注解也可以(本博客采用这个)* <p>* <p>* 两种方式* 方式一:加锁 固定时间内不能重复提交* <p>* 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交*/@Around("pointCutNoRepeatSubmit(repeatSubmit)")public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();//用于记录成功或者失败boolean res = false;//防重提交类型String type = repeatSubmit.limitType().name();if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {//方式一,参数形式防重提交} else {//方式二,令牌形式防重提交String requestToken = request.getHeader("request-token");if (StringUtils.isBlank(requestToken)) {throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);}String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, requestToken);/*** 提交表单的token key* 方式一:不用lua脚本获取再判断,之前是因为 key组成是 order:submit:accountNo, value是对应的token,所以需要先获取值,再判断* 方式二:可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成*/res = redisTemplate.delete(key);}if (!res) {log.error("请求重复提交");log.info("环绕通知中");return null;}log.info("环绕通知执行前");Object obj = joinPoint.proceed();log.info("环绕通知执行后");return obj;}
}

验证结果:

第一次请求后,执行正常查询筛选逻辑

再次请求同一个接口:


这样就完成了通过AOP token的防止重复提交

对AOP切面等不是很熟悉的,可以看下这篇博客中AOP的详细介绍以及实战举例:

AOP面向切面编程之全局日志打印/统计接口耗时_8年开发工作经验的老王,积极分享工作中遇到的问题~-CSDN博客

4.3.2 再看下参数的防重方式

参数式防重复的核心就是IP地址+类+方法+账号的方式,增加到redis中做为key。第一次加锁成功返回true,第二次返回false,通过这种来做到的防重复。

先介绍下Redission:
    Redission是一个在Redis的基础上实现的Java驻内存数据网格,支持多样Redis配置支持、丰富连接方式、分布式对象、分布式集合、分布式锁、分布式服务、多种序列化方式、三方框架整合
    Redisson底层采用的是Netty 框架
    官方文档:https://github.com/redisson/redisson

关于用redis来实现分布式锁的方式redis的api或者redis+lua或者Redission,可以看下这篇博客:

高并发下Redis实现分布式锁的坑你是否踩过_8年开发工作经验的老王,积极分享工作中遇到的问题~-CSDN博客

参数防重复加入Redission上代码环节

第一步 引入依赖pom.xml:

<!--分布式锁-->
<dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>3.10.1</version>
</dependency>

第二步 增加配置:

#-------redis连接配置-------
spring.redis.client-type=jedis
spring.redis.host=120.79.xxx.xxx
spring.redis.password=123456
spring.redis.port=6379
spring.redis.jedis.pool.max-active=100
spring.redis.jedis.pool.max-idle=100
spring.redis.jedis.pool.min-idle=100
spring.redis.jedis.pool.max-wait=60000

第三步 获取redissonClient:

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class RedissionConfiguration {@Value("${spring.redis.host}")private String redisHost;@Value("${spring.redis.port}")private String redisPort;@Value("${spring.redis.password}")private String redisPwd;/*** 配置分布式锁的redisson* @return*/@Beanpublic RedissonClient redissonClient(){Config config = new Config();//单机方式config.useSingleServer().setPassword(redisPwd).setAddress("redis://"+redisHost+":"+redisPort);//集群//config.useClusterServers().addNodeAddress("redis://192.31.21.1:6379","redis://192.31.21.2:6379")RedissonClient redissonClient = Redisson.create(config);return redissonClient;}/*** 集群模式* 备注:可以用"rediss://"来启用SSL连接*//*@Beanpublic RedissonClient redissonClusterClient() {Config config = new Config();config.useClusterServers().setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒.addNodeAddress("redis://127.0.0.1:7000").addNodeAddress("redis://127.0.0.1:7002");RedissonClient redisson = Redisson.create(config);return redisson;}*/}
第四步切面参数防重逻辑:

import lombok.extern.slf4j.Slf4j;
import net.wnn.annotation.RepeatSubmit;
import net.wnn.constant.RedisKey;
import net.wnn.enums.BizCodeEnum;
import net.wnn.exception.BizException;
import net.wnn.interceptor.LoginInterceptor;
import net.wnn.util.CommonUtil;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;/*** 定义一个切面类**/
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate RedissonClient redissonClient;/*** 定义 @Pointcut注解表达式, 通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法* 在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点* 方式一:@annotation:当执行的方法上拥有指定的注解时生效(本博客采用这)* 方式二:execution:一般用于指定方法的执行*/@Pointcut("@annotation(repeatSubmit)")public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {}/*** 环绕通知, 围绕着方法执行* @param joinPoint* @param repeatSubmit* @return* @throws Throwable* @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。* <p>* 方式一:单用 @Around("execution(* net.wnn.controller.*.*(..))")可以* 方式二:用@Pointcut和@Around联合注解也可以(本博客采用这个)* <p>* <p>* 两种方式* 方式一:加锁 固定时间内不能重复提交* <p>* 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交*/@Around("pointCutNoRepeatSubmit(repeatSubmit)")public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();//用于记录成功或者失败boolean res = false;//防重提交类型String type = repeatSubmit.limitType().name();if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {//方式一,参数形式防重提交long lockTime = repeatSubmit.lockTime();String ipAddr = CommonUtil.getIpAddr(request);MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();Method method = methodSignature.getMethod();String className = method.getDeclaringClass().getName();String key = "order-server:repeat_submit:"+CommonUtil.MD5(String.format("%s-%s-%s-%s",ipAddr,className,method,accountNo));//加锁// 这种也可以 本博客也介绍下redisson的使用// res  = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);RLock lock = redissonClient.getLock(key);// 尝试加锁,最多等待0秒,上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义]res = lock.tryLock(0,lockTime,TimeUnit.SECONDS);} else {//方式二,令牌形式防重提交String requestToken = request.getHeader("request-token");if (StringUtils.isBlank(requestToken)) {throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);}String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, requestToken);/*** 提交表单的token key* 方式一:不用lua脚本获取再判断,之前是因为 key组成是 order:submit:accountNo, value是对应的token,所以需要先获取值,再判断* 方式二:可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成*/res = redisTemplate.delete(key);}if (!res) {log.error("请求重复提交");log.info("环绕通知中");return null;}log.info("环绕通知执行前");Object obj = joinPoint.proceed();log.info("环绕通知执行后");return obj;}
}
其中lock.tryLock解释下:

// 尝试加锁,最多等待0秒,上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义]
            res = lock.tryLock(0,lockTime,TimeUnit.SECONDS);

tryLock只有在调用时空闲的情况下,才会获得该锁。如果锁可用,则获取该锁,并立即返回值为true;如果锁不可用,那么这个方法将立即返回值为false。

典型的用法:

这种用法可以保证在获得了锁的情况下解锁,在没有获得锁的情况下不尝试解锁。

第五步 使用

依然是在分页这块做个验证 看起来比较清晰

type改成RepeatSubmit.Type.PARAM

    /*** 分页接口** @return*/@PostMapping("page")@RepeatSubmit(limitType = RepeatSubmit.Type.PARAM)public JsonData page(@RequestBody ProductOrderPageRequest orderPageRequest) {Map<String, Object> pageResult = productOrderService.page(orderPageRequest);return JsonData.buildSuccess(pageResult);}

postman请求接口进行验证:

第一次请求后,redis的key中存在的,TTL 5秒

5秒内重复点击接口 因为已经存在的这个key,所以当再次增加key的时候,就会返回flase:

这样就完成了通过AOP 参数的防止重复提交

两种防重提交,应用场景不一样,也可以更多方式进行防重,根据实际业务进行选择即可

AOP+自定义注解token令牌和参数防重复提交实战相关推荐

  1. springboot+aop+自定义注解,打造通用的全局异常处理和参数校验切面(通用版)

    springboot+aop+自定义注解,打造通用的全局异常处理和参数校验切面(通用版) 参考文章: (1)springboot+aop+自定义注解,打造通用的全局异常处理和参数校验切面(通用版) ( ...

  2. Spring AOP自定义注解并获取注解的参数

    环境 springboot:1.5 Intellij IDEA:2021.1 序言 最近有个需求,要做方法层面的权限控制.以前在公司使用的是spring security,然后使用注解 如下: @Pr ...

  3. Java AOP自定义注解

    一.背景 在之前文章:Java注解详解中,主要介绍了注解的含义.作用.以及常用的各类注解.今天主要介绍在Springboot中如何实现一个自定义注解,通过自定义注解去实现一些定制化的需求. 二.了解元 ...

  4. spring AOP自定义注解方式实现日志管理

    转:spring AOP自定义注解方式实现日志管理 今天继续实现AOP,到这里我个人认为是最灵活,可扩展的方式了,就拿日志管理来说,用Spring AOP 自定义注解形式实现日志管理.废话不多说,直接 ...

  5. spring aop 自定义注解配合swagger注解保存操作日志到mysql数据库含(源码)

    spring aop 自定义注解保存操作日志到mysql数据库 一.思路 二.自定义注解 三.编写操作日志 四.编写操作日志切面\增强 五.使用 六.`注意` 一.思路 利用spring aop 对方 ...

  6. JPOM - AOP+自定义注解实现操作日志记录

    文章目录 地址 版本 源码解析-AOP+自定义注解实现操作日志记录 地址 Gitee: https://gitee.com/dromara/Jpom 官网: https://jpom.io/ 一款简而 ...

  7. java中注解动态传参_Java自定义注解源码+原理解释(使用Java自定义注解校验bean传入参数合法性)...

    Java自定义注解源码+原理解释(使用Java自定义注解校验bean传入参数合法性)java 前言:因为前段时间忙于写接口,在接口中须要作不少的参数校验,本着简洁.高效的原则,便写了这个小工具供本身使 ...

  8. AOP+自定义注解实现字典翻译

    目录 需求: 结果展示 : 代码实现 : 需求: 一般情况下数据库表中字段对应的是字典值,但在查询或导出时需要展示字典名,若每次字典转换时都需要关联字典表查询,而且导出时还需要在做一次数据转换,这样处 ...

  9. redis+aop防重复提交

    文章目录 1.防重复提交注解 2.redis分布式锁 3.防止重复提交Aop 之前有记录一篇用redis+拦截器防重复提交的内容: redis+拦截器防重复提交 1.防重复提交注解 @Target(E ...

最新文章

  1. 第一章 java nio三大组件与使用姿势
  2. MongoDb分片集群认证
  3. 论文阅读 - AUTOVC: Zero-Shot Voice Style Transfer with Only Autoencoder Loss
  4. qt中设置QCheckBox的文本与勾选框之间的距离
  5. Matlab实现Sandbox方法计算分形维数
  6. JS 缓存: Service Worker 实现离线应用
  7. php 并列排名,MySQL并列排名和顺序排名查询
  8. Mac如何读写外接硬盘?
  9. 关键词提取:TF-IDF和n-gram
  10. 我的世界java边境之地_我的世界边境之地是否存在 我的世界边境之地大揭秘
  11. 项目开发中之如何对接
  12. component动态组件
  13. 基于网络的入侵检测数据集研究综述(A Survey of Network-based Intrusion Detection Data Sets)
  14. esxi6.0虚拟机克隆及跨版本克隆
  15. 解决 SecureCRT 和 SecureFX 中文乱码
  16. [笔记][总结] MIT线性代数 Gilbert Strang 对称矩阵
  17. python 笔记:dtw包
  18. python库作用_python 里 certifi 库的作用
  19. linux rm 命令详解,Linux rm命令使用指南
  20. 戴尔R730服务器,双电源主用、备用功能设备。

热门文章

  1. linux 终端 朗读,使Linux终端朗读文字的小技巧分享,linux终端
  2. SECOND: Sparsely Embedded Convolutional Detection 论文笔记
  3. Unity 坐标归零按钮小工具
  4. 字体安装成功但是wps不显示?
  5. Java版的IRR(内部收益率)实现
  6. Python打包文件到exe 修改图标 优化启动速度
  7. MahApps.Metro扁平化UI控件库(可修改主题色等)
  8. window.showModalDialog();会弹出当前页面脚本发生错误部分解决方法
  9. 仿win10环境变量助手
  10. Python中10个常用的内置函数