1.哪些因素会引起重复提交?
- 前端下单按钮重复点击导致订单创建多次
- 网速等原因造成页面卡顿,用户重复刷新提交请求
- 黑客或恶意用户使用postman等http工具重复恶意提交表单
2.重复提交会带来哪些问题?
- 会导致表单重复提交,造成数据重复或者错乱
- 核心接口的请求增加,会消耗服务器负载,严重甚至会造成服务器宕机
3.订单的防重复提交你能想到几种方案?
方式一:前端JS控制点击次数,屏蔽点击按钮无法点击
前端可以被绕过,前端有限制,后端也需要有限制
方式二:数据库或者其他存储增加唯一索引约束
需要想出满足业务需求的唯一索引约束,比如注册的手机号唯一。但是有些业务是没有唯一性限制的,且重复提交也会导致数据错乱,比如你在电商平台可以买一部手机,也可以买两部手机
方式三:服务端token令牌方式下单前先获取令牌-存储redis,下单时一并把token提交并检验和删除-lua脚本
分布式情况下,采用Lua脚本进行操作(保障原子性)
其中方式三 是大家采用得最多的,那有没更加优雅的方式呢?
假如系统中不止一个地方,需要用到这种防重复提交,每一次都要写这种lua脚本,代码耦合性太强,这种又不属于业务逻辑,所以不推荐耦合进service中,可读性较低。
4.自定义注解+AOP
4.1 AOP+自定义注解接口防重提交多场景设计
防重提交方式
- token令牌方式
- ip+类+方法方式(方法参数)
利用AOP来实现
- Aspect Oriented Program 面向切面编程, 在不改变原有逻辑上增加额外的功能
- AOP思想把功能分两个部分,分离系统中的各种关注点
优点

- 减少代码侵入,解耦
- 可以统一处理横切逻辑,方便添加和删除横切逻辑
4.2 流程

5.代码实现
依赖
<!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--jedis--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.2.2</version> </dependency>
讯享网
配置
讯享网server: port: 9004 spring: redis: database: 0 host: localhost # redis服务器地址 port: 6379 # redis端口 password: jedis: pool: max-active: 50 # 连接池最大连接数(使用负值表示没有限制) min-idle: 0 # 连接池中最小空闲连接 max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制),因为配置了timeout,会以timeout为准 max-idle: 50 # 连接池中的最大空闲连接 timeout: 1200 # 连接超时时间(单位:毫秒)
5.1 自定义注解
package com.kang.redis.annotation; import java.lang.annotation.*; / * @Author Emperor Kang * @ClassName NonRepeatSubmit * @Description 防止重复提交注解 * @Date 2022/8/24 15:03 * @Version 1.0 * @Motto 让营地比你来时更干净 */ @Documented @Target(ElementType.METHOD) //应用在方法上 @Retention(RetentionPolicy.RUNTIME) //保留到虚拟机运行时,可通过反射获取 public @interface NonRepeatSubmit { / * 支持两种防重复提交方式: * 1.方法参数 * 2.令牌 */ enum Type {PARAM,TOKEN} / * 默认防重复提交,是方法参数 * @return */ Type limitType() default Type.PARAM; / * 加锁过期时间,默认是5s * @return */ long lockTime() default 5; }
5.2 编辑切面
讯享网package com.kang.redis.aop; import com.kang.redis.annotation.NonRepeatSubmit; import com.kang.redis.exception.ConfirmTokenException; import lombok.extern.slf4j.Slf4j; 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.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 static com.kang.redis.constant.RedisKeyConstant.SUBMIT_ORDER_TOKEN_KEY; / * @Author Emperor Kang * @ClassName NonRepeatSubmitAspect * @Description 利用切面对使用自定义注解的地方防止重复提交 * @Date 2022/8/24 15:44 * @Version 1.0 * @Motto 让营地比你来时更干净 */ @Aspect @Component @Slf4j @SuppressWarnings("all") public class NonRepeatSubmitAspect { @Autowired private StringRedisTemplate redisTemplate; / * 定义 @Pointcut注解表达式, 通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法 * 在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点 * 方式一:@annotation:当执行的方法上拥有指定的注解时生效(本次采用该方法) * 方式二:execution:一般用于指定方法的执行 */ @Pointcut("@annotation(nonRepeatSubmit)") public void pointCutNonRepeatSubmit(NonRepeatSubmit nonRepeatSubmit){} / * 环绕通知,围绕方法执行 * 两种环绕方式: * 方式一:单用 @Around("execution(* com.kang.redis.controller.*.*(..))")可以 * 方式二:用@Pointcut和@Around联合注解也可以(本地采用这个) * 防重复提交的两种方式 * 方式一:加锁 固定时间内不能重复提交 * 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交 * @param joinPoint * @param nonRepeatSubmit * @return */ @Around("pointCutNonRepeatSubmit(nonRepeatSubmit)") public Object around(ProceedingJoinPoint joinPoint,NonRepeatSubmit nonRepeatSubmit){ try { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if(servletRequestAttributes == null){ throw new ConfirmTokenException("AOP拦截:无法获取当前请求request"); } HttpServletRequest request = servletRequestAttributes.getRequest(); //一般都是从request中获取当前用户ID String userId = "0001"; //用于记录成功还是失败 boolean result = false; //防重复提交的类型 String type = nonRepeatSubmit.limitType().name(); //注解中有默认配置所以不用考虑isBlank的情况 if(type.equals(NonRepeatSubmit.Type.PARAM.name())){ //方式一,参数形式防重提交 log.info("AOP拦截:采用参数形式防重复提交"); } else { //方式二,令牌形式防重提交 String token = request.getHeader("token"); if(StringUtils.isBlank(token)){ throw new ConfirmTokenException("AOP拦截:token为空,非法请求"); } String key = String.format(SUBMIT_ORDER_TOKEN_KEY,userId,token); / * 只有第一次提交时才会删除成功 * 方式一:不用lua脚本获取再判断,之前是因为 key组成是 order:submit:accountNo, value是对应的token,所以需要先获取值,再判断 * 方式二:可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成 */ result = redisTemplate.delete(key); } if(!result){ log.error("AOP拦截:请求重复提交"); log.info("AOP拦截:环绕该方法进行通知"); throw new ConfirmTokenException("AOP拦截:请求重复提交"); } log.info("AOP拦截:方法执行前"); Object object = joinPoint.proceed(); log.info("AOP拦截:方法执行后获得结果为:{}",object); return object; } catch (Throwable e) { log.error("AOP拦截:执行出错",e); return e.getMessage(); } } }
5.3 自定义异常类
package com.kang.redis.exception; / * @Author Emperor Kang * @ClassName ConfirmToeknException * @Description 自定义异常 * @Date 2022/8/24 16:22 * @Version 1.0 * @Motto 让营地比你来时更干净 */ public class ConfirmTokenException extends Exception{ public ConfirmTokenException() { super(); } public ConfirmTokenException(String message) { super(message); } public ConfirmTokenException(String message, Throwable cause) { super(message, cause); } public ConfirmTokenException(Throwable cause) { super(cause); } protected ConfirmTokenException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }
5.4 redis的一些配置类(StringRedisTemplate用不到)
package com.kang.redis.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; / * @Author Emperor Kang * @ClassName RedisConfig * @Description redis配置类 * @Date 2022/8/11 11:17 * @Version 1.0 * @Motto 让营地比你来时更干净 */ @Configuration public class RedisConfig { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setValueSerializer(RedisSerializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); redisTemplate.setHashValueSerializer(RedisSerializer.string()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
5.4 控制器编写
package com.kang.redis.controller; import cn.hutool.core.lang.UUID; import com.alibaba.fastjson.JSON; import com.kang.redis.annotation.NonRepeatSubmit; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static com.kang.redis.constant.RedisKeyConstant.SUBMIT_ORDER_TOKEN_KEY; / * @Author Emperor Kang * @ClassName NonRepeatSubmitController * @Description 提前获取令牌用于防重复提交 * @Date 2022/8/24 15:16 * @Version 1.0 * @Motto 让营地比你来时更干净 */ @RestController @RequestMapping("/submit") @Slf4j public class NonRepeatSubmitController { @Autowired private StringRedisTemplate redisTemplate; / * 生成token * @return */ @GetMapping("token") public String getOrderToken(){ //假设该用户的userId="0001" String userId = "0001"; //用UUID作为token String token = UUID.randomUUID().toString().replaceAll("-",""); //组装存入redis的key String key = String.format(SUBMIT_ORDER_TOKEN_KEY,userId,token); log.info("生成的key为:{}",key); //令牌的有效时间是30分钟 redisTemplate.opsForValue().set(key,String.valueOf(Thread.currentThread().getId()),30, TimeUnit.MINUTES); return token; } / * add添加方法 * @return */ @PostMapping("add") @NonRepeatSubmit(limitType = NonRepeatSubmit.Type.TOKEN,lockTime = 10) public String getUserInfo(){ List<Object> db = new ArrayList<>(); Map<String,String> map = new HashMap<>(); map.put("name","齐景春"); map.put("age","10000"); map.put("message","插入成功"); db.add(map); log.info("该数据插入数据库成功"); return JSON.toJSONString(db); } }
6.测试Token的方式
获取token
第一次添加add
第二次添加
日志变化

其他部分后续再补充,token的方式至此结束
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/22374.html