基于SpringBoot接口+Redis解決用戶重復(fù)提交問題
前言
1. 為什么會(huì)出現(xiàn)用戶重復(fù)提交
- 網(wǎng)絡(luò)延遲的情況下用戶多次點(diǎn)擊submit按鈕導(dǎo)致表單重復(fù)提交;
- 用戶提交表單后,點(diǎn)擊【刷新】按鈕導(dǎo)致表單重復(fù)提交(點(diǎn)擊瀏覽器的刷新按鈕,就是把瀏覽器上次做的事情再做一次,因?yàn)檫@樣也會(huì)導(dǎo)致表單重復(fù)提交);
- 用戶提交表單后,點(diǎn)擊瀏覽器的【后退】按鈕回退到表單頁面后進(jìn)行再次提交。
2. 重復(fù)提交不攔截可能導(dǎo)致的問題
- 重復(fù)數(shù)據(jù)入庫,造成臟數(shù)據(jù)。即使數(shù)據(jù)庫表有UK索引,該操作也會(huì)增加系統(tǒng)的不必要負(fù)擔(dān);
- 會(huì)成為黑客爆破攻擊的入口,大量的請(qǐng)求會(huì)導(dǎo)致應(yīng)用崩潰;
- 用戶體驗(yàn)差,多條重復(fù)的數(shù)據(jù)還需要一條條的刪除等。
3. 解決辦法
辦法有很多,我這里只說一種,利用Redis的set方法搞定(不是redisson)
項(xiàng)目代碼
項(xiàng)目結(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ù)器連接密碼(默認(rèn)為空) spring.redis.password= # 連接池最大連接數(shù)(使用負(fù)值表示沒有限制) spring.redis.jedis.pool.max-active=20 # 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制) spring.redis.jedis.pool.max-wait=-1 # 連接池中的最大空閑連接 spring.redis.jedis.pool.max-idle=10 # 連接池中的最小空閑連接 spring.redis.jedis.pool.min-idle=0 # 連接超時(shí)時(shí)間(毫秒) 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 加上這個(gè)注解可以將參數(shù)也設(shè)置為key,唯一key來源 */ @Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface RequestKeyParam { /** * key值名稱 * * @return 默認(rèn)為空 */ String name() default ""; }
RequestLock.java
package com.example.requestlock.lock.annotation; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; /** * @description 請(qǐng)求防抖鎖,用于防止前端重復(fù)提交導(dǎo)致的錯(cuò)誤 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface RequestLock { /** * redis鎖前綴 * * @return 默認(rèn)為空,但不可為空 */ String prefix() default ""; /** * redis鎖過期時(shí)間 * * @return 默認(rèn)2秒 */ int expire() default 2; /** * redis鎖過期時(shí)間單位 * * @return 默認(rèn)單位為秒 */ 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 請(qǐng)求鎖切面處理器 */ @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("重復(fù)提交前綴不能為空"); return "重復(fù)提交前綴不能為空"; } //獲取自定義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("您的操作太快了,請(qǐng)稍后重試"); return "您的操作太快了,請(qǐng)稍后重試"; } 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 切入點(diǎn) * @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) { //獲取連接點(diǎn)的方法簽名對(duì)象 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); //Method對(duì)象 Method method = methodSignature.getMethod(); //獲取Method對(duì)象上的注解對(duì)象 RequestLock requestLock = method.getAnnotation(RequestLock.class); //獲取方法參數(shù) final Object[] args = joinPoint.getArgs(); //獲取Method對(duì)象上所有的注解 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())) { //獲取方法上的多個(gè)注解(為什么是兩層數(shù)組:因?yàn)榈诙訑?shù)組是只有一個(gè)元素的數(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時(shí)可以使用反射訪問私有變量,否則不能訪問私有變量) 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(請(qǐng)求鎖)利用了Redis的單線程處理以及Key值過期特點(diǎn),核心通過RequestLock、RequestKeyParam注解生成一個(gè)唯一的key值,存入redis后設(shè)置一個(gè)過期時(shí)間(1-3秒),當(dāng)?shù)诙握?qǐng)求的時(shí)候,判斷生成的key值是否在Redis中存在,如果存在則認(rèn)為第二次提交是重復(fù)的。
流程圖如下:
用法說明
1. 在controller的方法上增加@RequestLock注解,并給一個(gè)前綴
@PostMapping("/addUser2") @RequestLock(prefix = "addUser") public String addUser2(@RequestBody User user)
加了@RequestLock注解代表這個(gè)方法會(huì)進(jìn)行重復(fù)提交校驗(yàn),沒有加則不會(huì)進(jìn)行校驗(yàn)。通過注解的方式可以使用法變得靈活。
2. @RequestKeyParam注解用在對(duì)象的屬性上
@RequestKeyParam(name = "phone") private String phone;
在對(duì)象的屬性上加@RequestKeyParam注解后,Redis的key則由 @RequestLock定義的prefix加上字段的值組成,比如當(dāng)傳入傳入phone是123456789,那么當(dāng)前的key值則為: addUser:123456789
。
效果展示
調(diào)用addUser1接口
這里無論點(diǎn)擊多少次提交,都會(huì)展示添加“添加成功”,這樣是不行的。
調(diào)用addUser2接口
第一次提交,“添加成功”。
快速點(diǎn)擊第二次提交,就會(huì)出現(xiàn)“您的操作太快了,請(qǐng)稍后重試”提示。
以上就是基于SpringBoot接口+Redis解決用戶重復(fù)提交問題的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot+Redis解決重復(fù)提交的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot+Redis大量重復(fù)提交問題的解決方案
- SpringBoot利用Redis解決海量重復(fù)提交問題
- SpringBoot+Redisson自定義注解一次解決重復(fù)提交問題
- SpringBoot+Redis海量重復(fù)提交問題解決
- SpringBoot整合redis+Aop防止重復(fù)提交的實(shí)現(xiàn)
- SpringBoot+Redis使用AOP防止重復(fù)提交的實(shí)現(xiàn)
- SpringBoot?使用AOP?+?Redis?防止表單重復(fù)提交的方法
- SpringBoot基于redis自定義注解實(shí)現(xiàn)后端接口防重復(fù)提交校驗(yàn)
- SpringBoot?+?Redis如何解決重復(fù)提交問題(冪等)
- SpringBoot+Redis實(shí)現(xiàn)后端接口防重復(fù)提交校驗(yàn)的示例
- Spring Boot通過Redis實(shí)現(xiàn)防止重復(fù)提交
相關(guān)文章
Spring創(chuàng)建Bean的過程Debug的詳細(xì)流程
這篇文章主要介紹了Spring創(chuàng)建Bean的過程Debug的流程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11mybatisplus的連表增強(qiáng)插件mybatis plus join
本文主要介紹了mybatisplus的連表增強(qiáng)插件mybatis plus join,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06Shiro與Springboot整合開發(fā)的基本步驟過程詳解
這篇文章主要介紹了Shiro與Springboot整合開發(fā)的基本步驟,本文結(jié)合實(shí)例代碼給大家介紹整合過程,感興趣的朋友跟隨小編一起看看吧2023-06-06解決JavaWeb-file.isDirectory()遇到的坑問題
JavaWeb開發(fā)中,使用`file.isDirectory()`判斷路徑是否為文件夾時(shí),需要特別注意:該方法只能判斷已存在的文件夾,若路徑不存在,無論其實(shí)際是否應(yīng)為文件夾,均會(huì)返回`false`,為了解決這個(gè)問題,可以采用正則表達(dá)式進(jìn)行判斷,但要求路徑字符串的結(jié)尾必須添加反斜杠(\)2025-02-02IDEA下SpringBoot指定配置文件啟動(dòng)項(xiàng)目的全過程
我們?cè)谑褂胹pringboot項(xiàng)目開發(fā)的時(shí)候,每次切換環(huán)境跑項(xiàng)目的時(shí)候,都得修改配置文件的數(shù)據(jù)庫地址,這樣來回修改感覺很麻煩,這篇文章主要給大家介紹了關(guān)于IDEA下SpringBoot指定配置文件啟動(dòng)項(xiàng)目的相關(guān)資料,需要的朋友可以參考下2023-06-06淺析我對(duì) String、StringBuilder、StringBuffer 的理解
StringBuilder、StringBuffer 和 String 一樣,都是用于存儲(chǔ)字符串的。這篇文章談?wù)勑【帉?duì)String、StringBuilder、StringBuffer 的理解,感興趣的朋友跟隨小編一起看看吧2020-05-05