基于SpringBoot接口+Redis解决用户重复提交问题
作者:summo
当网络延迟的情况下用户多次点击submit按钮导致表单重复提交,用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交也会出现用户重复提交,办法有很多,我这里只说一种,利用Redis的set方法搞定,需要的朋友可以参考下
前言
1. 为什么会出现用户重复提交
- 网络延迟的情况下用户多次点击submit按钮导致表单重复提交;
- 用户提交表单后,点击【刷新】按钮导致表单重复提交(点击浏览器的刷新按钮,就是把浏览器上次做的事情再做一次,因为这样也会导致表单重复提交);
- 用户提交表单后,点击浏览器的【后退】按钮回退到表单页面后进行再次提交。
2. 重复提交不拦截可能导致的问题
- 重复数据入库,造成脏数据。即使数据库表有UK索引,该操作也会增加系统的不必要负担;
- 会成为黑客爆破攻击的入口,大量的请求会导致应用崩溃;
- 用户体验差,多条重复的数据还需要一条条的删除等。
3. 解决办法
办法有很多,我这里只说一种,利用Redis的set方法搞定(不是redisson)
项目代码
项目结构
配置文件
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.9</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>RequestLock</artifactId> <version>0.0.1-SNAPSHOT</version> <name>RequestLock</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- redis依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- web依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 切面 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.5</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.properties
spring.application.name=RequestLock server.port=8080 # Redis服务器地址 spring.redis.host=127.0.0.1 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接池最大连接数(使用负值表示没有限制) spring.redis.jedis.pool.max-active=20 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.jedis.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.jedis.pool.max-idle=10 # 连接池中的最小空闲连接 spring.redis.jedis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=1000
代码文件
RequestLockApplication.java
package com.example.requestlock; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RequestLockApplication { public static void main(String[] args) { SpringApplication.run(RequestLockApplication.class, args); } }
User.java
package com.example.requestlock.model; import com.example.requestlock.lock.annotation.RequestKeyParam; public class User { private String name; private Integer age; @RequestKeyParam(name = "phone") private String phone; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + ", phone='" + phone + '\'' + '}'; } }
RequestKeyParam.java
package com.example.requestlock.lock.annotation; import java.lang.annotation.*; /** * @description 加上这个注解可以将参数也设置为key,唯一key来源 */ @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface RequestKeyParam { /** * key值名称 * * @return 默认为空 */ String name() default ""; }
RequestLock.java
package com.example.requestlock.lock.annotation; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; /** * @description 请求防抖锁,用于防止前端重复提交导致的错误 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface RequestLock { /** * redis锁前缀 * * @return 默认为空,但不可为空 */ String prefix() default ""; /** * redis锁过期时间 * * @return 默认2秒 */ int expire() default 2; /** * redis锁过期时间单位 * * @return 默认单位为秒 */ TimeUnit timeUnit() default TimeUnit.SECONDS; /** * redis key分隔符 * * @return 分隔符 */ String delimiter() default ":"; }
RequestLockMethodAspect.java
package com.example.requestlock.lock.aspect; import com.example.requestlock.lock.annotation.RequestLock; import com.example.requestlock.lock.keygenerator.RequestKeyGenerator; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.types.Expiration; import org.springframework.util.StringUtils; import java.lang.reflect.Method; /** * @description 请求锁切面处理器 */ @Aspect @Configuration public class RequestLockMethodAspect { private final StringRedisTemplate stringRedisTemplate; private final RequestKeyGenerator requestKeyGenerator; @Autowired public RequestLockMethodAspect(StringRedisTemplate stringRedisTemplate, RequestKeyGenerator requestKeyGenerator) { this.requestKeyGenerator = requestKeyGenerator; this.stringRedisTemplate = stringRedisTemplate; } @Around("execution(public * * (..)) && @annotation(com.example.requestlock.lock.annotation.RequestLock)") public Object interceptor(ProceedingJoinPoint joinPoint) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); RequestLock requestLock = method.getAnnotation(RequestLock.class); if (StringUtils.isEmpty(requestLock.prefix())) { // throw new RuntimeException("重复提交前缀不能为空"); return "重复提交前缀不能为空"; } //获取自定义key final String lockKey = requestKeyGenerator.getLockKey(joinPoint); final Boolean success = stringRedisTemplate.execute( (RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), new byte[0], Expiration.from(requestLock.expire(), requestLock.timeUnit()) , RedisStringCommands.SetOption.SET_IF_ABSENT)); if (!success) { // throw new RuntimeException("您的操作太快了,请稍后重试"); return "您的操作太快了,请稍后重试"; } try { return joinPoint.proceed(); } catch (Throwable throwable) { // throw new RuntimeException("系统异常"); return "系统异常"; } } }
RequestKeyGenerator.java
package com.example.requestlock.lock.keygenerator; import org.aspectj.lang.ProceedingJoinPoint; /** * 加锁key生成器 */ public interface RequestKeyGenerator { /** * 获取AOP参数,生成指定缓存Key * * @param joinPoint 切入点 * @return 返回key值 */ String getLockKey(ProceedingJoinPoint joinPoint); }
RequestKeyGeneratorImpl.java
package com.example.requestlock.lock.keygenerator.impl; import com.example.requestlock.lock.annotation.RequestKeyParam; import com.example.requestlock.lock.annotation.RequestLock; import com.example.requestlock.lock.keygenerator.RequestKeyGenerator; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Service; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Parameter; @Service public class RequestKeyGeneratorImpl implements RequestKeyGenerator { @Override public String getLockKey(ProceedingJoinPoint joinPoint) { //获取连接点的方法签名对象 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); //Method对象 Method method = methodSignature.getMethod(); //获取Method对象上的注解对象 RequestLock requestLock = method.getAnnotation(RequestLock.class); //获取方法参数 final Object[] args = joinPoint.getArgs(); //获取Method对象上所有的注解 final Parameter[] parameters = method.getParameters(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < parameters.length; i++) { final RequestKeyParam cacheParams = parameters[i].getAnnotation(RequestKeyParam.class); //如果属性不是CacheParam注解,则不处理 if (cacheParams == null) { continue; } //如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam sb.append(requestLock.delimiter()).append(args[i]); } //如果方法上没有加CacheParam注解 if (StringUtils.isEmpty(sb.toString())) { //获取方法上的多个注解(为什么是两层数组:因为第二层数组是只有一个元素的数组) final Annotation[][] parameterAnnotations = method.getParameterAnnotations(); //循环注解 for (int i = 0; i < parameterAnnotations.length; i++) { final Object object = args[i]; //获取注解类中所有的属性字段 final Field[] fields = object.getClass().getDeclaredFields(); for (Field field : fields) { //判断字段上是否有CacheParam注解 final RequestKeyParam annotation = field.getAnnotation(RequestKeyParam.class); //如果没有,跳过 if (annotation == null) { continue; } //如果有,设置Accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量) field.setAccessible(true); //如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam sb.append(requestLock.delimiter()).append(ReflectionUtils.getField(field, object)); } } } //返回指定前缀的key return requestLock.prefix() + sb; } }
UserController.java
package com.example.requestlock.controller; import com.example.requestlock.lock.annotation.RequestLock; import com.example.requestlock.model.User; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @PostMapping("/addUser1") public String addUser1(@RequestBody User user) { System.out.println("不做任何处理" + user); return "添加成功"; } @PostMapping("/addUser2") @RequestLock(prefix = "addUser") public String addUser2(@RequestBody User user) { System.out.println("防重提交" + user); return "添加成功"; } }
原理解释
该RequestLock(请求锁)利用了Redis的单线程处理以及Key值过期特点,核心通过RequestLock、RequestKeyParam注解生成一个唯一的key值,存入redis后设置一个过期时间(1-3秒),当第二次请求的时候,判断生成的key值是否在Redis中存在,如果存在则认为第二次提交是重复的。
流程图如下:
用法说明
1. 在controller的方法上增加@RequestLock注解,并给一个前缀
@PostMapping("/addUser2") @RequestLock(prefix = "addUser") public String addUser2(@RequestBody User user)
加了@RequestLock注解代表这个方法会进行重复提交校验,没有加则不会进行校验。通过注解的方式可以使用法变得灵活。
2. @RequestKeyParam注解用在对象的属性上
@RequestKeyParam(name = "phone") private String phone;
在对象的属性上加@RequestKeyParam注解后,Redis的key则由 @RequestLock定义的prefix加上字段的值组成,比如当传入传入phone是123456789,那么当前的key值则为: addUser:123456789
。
效果展示
调用addUser1接口
这里无论点击多少次提交,都会展示添加“添加成功”,这样是不行的。
调用addUser2接口
第一次提交,“添加成功”。
快速点击第二次提交,就会出现“您的操作太快了,请稍后重试”提示。
以上就是基于SpringBoot接口+Redis解决用户重复提交问题的详细内容,更多关于SpringBoot+Redis解决重复提交的资料请关注脚本之家其它相关文章!
您可能感兴趣的文章:
- SpringBoot+Redis大量重复提交问题的解决方案
- SpringBoot利用Redis解决海量重复提交问题
- SpringBoot+Redisson自定义注解一次解决重复提交问题
- SpringBoot+Redis海量重复提交问题解决
- SpringBoot整合redis+Aop防止重复提交的实现
- SpringBoot+Redis使用AOP防止重复提交的实现
- SpringBoot 使用AOP + Redis 防止表单重复提交的方法
- SpringBoot基于redis自定义注解实现后端接口防重复提交校验
- SpringBoot + Redis如何解决重复提交问题(幂等)
- SpringBoot+Redis实现后端接口防重复提交校验的示例
- Spring Boot通过Redis实现防止重复提交