基于SpringBoot接口+Redis解決用戶重復提交問題
前言
1. 為什么會出現(xiàn)用戶重復提交
- 網(wǎng)絡(luò)延遲的情況下用戶多次點擊submit按鈕導致表單重復提交;
- 用戶提交表單后,點擊【刷新】按鈕導致表單重復提交(點擊瀏覽器的刷新按鈕,就是把瀏覽器上次做的事情再做一次,因為這樣也會導致表單重復提交);
- 用戶提交表單后,點擊瀏覽器的【后退】按鈕回退到表單頁面后進行再次提交。
2. 重復提交不攔截可能導致的問題
- 重復數(shù)據(jù)入庫,造成臟數(shù)據(jù)。即使數(shù)據(jù)庫表有UK索引,該操作也會增加系統(tǒng)的不必要負擔;
- 會成為黑客爆破攻擊的入口,大量的請求會導致應(yīng)用崩潰;
- 用戶體驗差,多條重復的數(shù)據(jù)還需要一條條的刪除等。
3. 解決辦法
辦法有很多,我這里只說一種,利用Redis的set方法搞定(不是redisson)
項目代碼
項目結(jié)構(gòu)

配置文件
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服務(wù)器地址 spring.redis.host=127.0.0.1 # Redis服務(wù)器連接端口 spring.redis.port=6379 # Redis服務(wù)器連接密碼(默認為空) spring.redis.password= # 連接池最大連接數(shù)(使用負值表示沒有限制) 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 加上這個注解可以將參數(shù)也設(shè)置為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("系統(tǒng)異常");
return "系統(tǒng)異常";
}
}
}
RequestKeyGenerator.java
package com.example.requestlock.lock.keygenerator;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 加鎖key生成器
*/
public interface RequestKeyGenerator {
/**
* 獲取AOP參數(shù),生成指定緩存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);
//獲取方法參數(shù)
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())) {
//獲取方法上的多個注解(為什么是兩層數(shù)組:因為第二層數(shù)組是只有一個元素的數(shù)組)
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
//循環(huán)注解
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;
}
//如果有,設(shè)置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后設(shè)置一個過期時間(1-3秒),當?shù)诙握埱蟮臅r候,判斷生成的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。
效果展示
調(diào)用addUser1接口

這里無論點擊多少次提交,都會展示添加“添加成功”,這樣是不行的。
調(diào)用addUser2接口

第一次提交,“添加成功”。

快速點擊第二次提交,就會出現(xiàn)“您的操作太快了,請稍后重試”提示。
以上就是基于SpringBoot接口+Redis解決用戶重復提交問題的詳細內(nèi)容,更多關(guān)于SpringBoot+Redis解決重復提交的資料請關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot+Redis大量重復提交問題的解決方案
- SpringBoot利用Redis解決海量重復提交問題
- SpringBoot+Redisson自定義注解一次解決重復提交問題
- SpringBoot+Redis海量重復提交問題解決
- SpringBoot整合redis+Aop防止重復提交的實現(xiàn)
- SpringBoot+Redis使用AOP防止重復提交的實現(xiàn)
- SpringBoot?使用AOP?+?Redis?防止表單重復提交的方法
- SpringBoot基于redis自定義注解實現(xiàn)后端接口防重復提交校驗
- SpringBoot?+?Redis如何解決重復提交問題(冪等)
- SpringBoot+Redis實現(xiàn)后端接口防重復提交校驗的示例
- Spring Boot通過Redis實現(xiàn)防止重復提交
相關(guān)文章
Java實現(xiàn)輕松提取word和pdf文檔內(nèi)容
這篇文章主要為大家詳細介紹了如何使用Java實現(xiàn)輕松提取word和pdf文檔內(nèi)容,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下2025-10-10
springboot攔截器過濾token,并返回結(jié)果及異常處理操作
這篇文章主要介紹了springboot攔截器過濾token,并返回結(jié)果及異常處理操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09
java使用hashMap緩存保存數(shù)據(jù)的方法
這篇文章主要介紹了java使用hashMap緩存保存數(shù)據(jù)的方法,結(jié)合實例形式簡單分析了java基于hashmap讀寫緩存數(shù)據(jù)的相關(guān)操作技巧,需要的朋友可以參考下2016-08-08

