java開(kāi)發(fā)中防止重復(fù)提交的幾種解決方案
一、產(chǎn)生原因
對(duì)于重復(fù)提交的問(wèn)題,主要由于重復(fù)點(diǎn)擊或者網(wǎng)絡(luò)重發(fā)請(qǐng)求, 我要先了解產(chǎn)生原因幾種方式:
- 點(diǎn)擊提交按鈕兩次;
- 點(diǎn)擊刷新按鈕;
- 使用瀏覽器后退按鈕重復(fù)之前的操作,導(dǎo)致重復(fù)提交表單;
- 使用瀏覽器歷史記錄重復(fù)提交表單;
- 瀏覽器重復(fù)的HTTP請(qǐng);
- nginx重發(fā)等情況;
- 分布式RPC的try重發(fā)等點(diǎn)擊提交按鈕兩次;
- 等… …
二、冪等
對(duì)于重復(fù)提交的問(wèn)題 主要涉及到時(shí) 冪等 問(wèn)題,那么先說(shuō)一下什么是冪等。
冪等:F(F(X)) = F(X)多次運(yùn)算結(jié)果一致;簡(jiǎn)單點(diǎn)說(shuō)就是對(duì)于完全相同的操作,操作一次與操作多次的結(jié)果是一樣的。
在開(kāi)發(fā)中,我們都會(huì)涉及到對(duì)數(shù)據(jù)庫(kù)操作。例如:
select 查詢天然冪等
delete 刪除也是冪等,刪除同一個(gè)多次效果一樣
update 直接更新某個(gè)值(如:狀態(tài) 字段固定值),冪等
update 更新累加操作(如:商品數(shù)量 字段),非冪等
(可以采用簡(jiǎn)單的樂(lè)觀鎖和悲觀鎖 個(gè)人更喜歡樂(lè)觀鎖。
樂(lè)觀鎖:數(shù)據(jù)庫(kù)表加version字段的方式;
悲觀鎖:用了 select…for update 的方式,* 要使用悲觀鎖,我們必須關(guān)閉mysql數(shù)據(jù)庫(kù)的自動(dòng)提交屬性。
這種在大數(shù)據(jù)量和高并發(fā)下效率依賴數(shù)據(jù)庫(kù)硬件能力,可針對(duì)并發(fā)量不高的非核心業(yè)務(wù);)
insert 非冪等操作,每次新增一條 重點(diǎn) (數(shù)據(jù)庫(kù)簡(jiǎn)單方案:可采取數(shù)據(jù)庫(kù)唯一索引方式;這種在大數(shù)據(jù)量和高并發(fā)下效率依賴數(shù)據(jù)庫(kù)硬件能力,可針對(duì)并發(fā)量不高的非核心業(yè)務(wù);)
三、解決方案
1. 方案對(duì)比
| 序號(hào) | 前端/后端 | 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) | 代碼實(shí)現(xiàn) |
|---|---|---|---|---|---|
| 1) | 前端 | 前端js提交后禁止按鈕,返回結(jié)果后解禁等 | 簡(jiǎn)單 方便 | 只能控制頁(yè)面,通過(guò)工具可繞過(guò)不安全 | 略 |
| 2) | 后端 | 提交后重定向到其他頁(yè)面,防止用戶F5和瀏覽器前進(jìn)后退等重復(fù)提交問(wèn)題 | 簡(jiǎn)單 方便 | 體驗(yàn)不好,適用部分場(chǎng)景,若是遇到網(wǎng)絡(luò)問(wèn)題 還會(huì)出現(xiàn) | 略 |
| 3) | 后端 | 在表單、session、token 放入唯一標(biāo)識(shí)符(如:UUID),每次操作時(shí),保存標(biāo)識(shí)一定時(shí)間后移除,保存期間有相同的標(biāo)識(shí)就不處理或提示 | 相對(duì)簡(jiǎn)單 | 表單:有時(shí)需要前后端協(xié)商配合; session、token:加大服務(wù)性能開(kāi)銷 | 略 |
| 4) | 后端 | ConcurrentHashMap 、LRUMap 、google Cache 都是采用唯一標(biāo)識(shí)(如:用戶ID+請(qǐng)求路徑+參數(shù)) | 相對(duì)簡(jiǎn)單 | 適用于單機(jī)部署的應(yīng)用 | 見(jiàn)下 |
| 5) | 后端 | redis 是線程安全的,可以實(shí)現(xiàn)redis分布式鎖。設(shè)置唯一標(biāo)識(shí)(如:用戶ID+請(qǐng)求路徑+參數(shù))當(dāng)做key ,value值可以隨意(推薦設(shè)置成過(guò)期的時(shí)間點(diǎn)),在設(shè)置key的過(guò)期時(shí)間 | 單機(jī)、分布式、高并發(fā)都可以決絕 | 相對(duì)復(fù)雜需要部署維護(hù)redis | 見(jiàn)下 |
2. 代碼實(shí)現(xiàn)
4). google cache 代碼實(shí)現(xiàn) 注解方式 Single lock
pom.xml 引入
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
配置文件 .yml
resubmit:
local:
timeOut: 30
實(shí)現(xiàn)代碼
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LocalLock {
}
import com.alibaba.fastjson.JSONObject;
import com.example.mydemo.common.utils.IpUtils;
import com.example.mydemo.common.utils.Result;
import com.example.mydemo.common.utils.SecurityUtils;
import com.example.mydemo.common.utils.sign.MyMD5Util;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.Data;
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.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
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.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author: xx
* @description: 單機(jī)放重復(fù)提交
*/
@Data
@Aspect
@Configuration
public class LocalLockMethodInterceptor {
@Value("${spring.profiles.active}")
private String springProfilesActive;
@Value("${spring.application.name}")
private String springApplicationName;
private static int expireTimeSecond =5;
@Value("${resubmit:local:timeOut}")
public void setExpireTimeSecond(int expireTimeSecond) {
LocalLockMethodInterceptor.expireTimeSecond = expireTimeSecond;
}
//定義緩存,設(shè)置最大緩存數(shù)及過(guò)期日期
private static final Cache<String,Object> CACHE =
CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(expireTimeSecond, TimeUnit.SECONDS).build();
@Around("execution(public * *(..)) && @annotation(com.example.mydemo.common.interceptor.annotation.LocalLock)")
public Object interceptor(ProceedingJoinPoint joinPoint){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// LocalLock localLock = method.getAnnotation(LocalLock.class);
try{
String key = getLockUniqueKey(signature,joinPoint.getArgs());
if(CACHE.getIfPresent(key) != null){
return Result.fail("不允許重復(fù)提交,請(qǐng)稍后再試");
}
CACHE.put(key,key);
return joinPoint.proceed();
}catch (Throwable throwable){
throw new RuntimeException(throwable.getMessage());
}finally {
}
}
/**
* 獲取唯一標(biāo)識(shí)key
*
* @param methodSignature
* @param args
* @return
*/
private String getLockUniqueKey(MethodSignature methodSignature, Object[] args) throws NoSuchAlgorithmException {
//請(qǐng)求uri, 獲取類名稱,方法名稱
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
// HttpServletResponse responese = servletRequestAttributes.getResponse();
//獲取用戶信息
String userMsg = SecurityUtils.getUsername(); //獲取登錄用戶名稱
//1.判斷用戶是否登錄
if (StringUtils.isEmpty(userMsg)) { //未登錄用戶獲取真實(shí)ip
userMsg = IpUtils.getIpAddr(request);
}
String hash = "";
List list = new ArrayList();
if (args.length > 0) {
String[] parameterNames = methodSignature.getParameterNames();
for (int i = 0; i < parameterNames.length; i++) {
Object obj = args[i];
list.add(obj);
}
hash = JSONObject.toJSONString(list);
}
//項(xiàng)目名稱 + 環(huán)境編碼 + 獲取類名稱 + 方法名稱 + 唯一key
String key = "locallock:" + springApplicationName + ":" + springProfilesActive + ":" + userMsg + ":" + request.getRequestURI();
if (StringUtils.isNotEmpty(key)) {
key = key + ":" + hash;
}
key = MyMD5Util.getMD5(key);
return key;
}使用:
@LocalLock
public void save(@RequestBody User user) {
}
5)redis
pom.xml 引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
.yml文件 redis 配置
spring:
redis:
host: localhost
port: :6379
password: 123456
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisLock {
int expire() default 5;
}import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.heshu.sz.blockchain.utonhsbs.common.utils.MyMD5Util;
import com.heshu.sz.blockchain.utonhsbs.common.utils.SecurityUtils;
import com.heshu.sz.blockchain.utonhsbs.common.utils.ip.IpUtils;
import com.heshu.sz.blockchain.utonhsbs.framework.interceptor.annotation.RedisLock;
import com.heshu.sz.blockchain.utonhsbs.framework.system.domain.BaseResult;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.web.context.request.RequestAttributes;
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.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
/**
* @author :xx
* @description:
* @date : 2022/7/1 9:41
*/
@Slf4j
@Aspect
@Configuration
public class RedisLockMethodInterceptor {
@Value("${spring.profiles.active}")
private String springProfilesActive;
@Value("${spring.application.name}")
private String springApplicationName;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Pointcut("@annotation(com.heshu.sz.blockchain.utonhsbs.framework.interceptor.annotation.RedisLock)")
public void point() {
}
@Around("point()")
public Object doaround(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedisLock localLock = method.getAnnotation(RedisLock.class);
try {
String lockUniqueKey = getLockUniqueKey(signature, joinPoint.getArgs());
Integer expire = localLock.expire();
if (expire < 0) {
expire = 5;
}
ArrayList<String> keys = Lists.newArrayList(lockUniqueKey);
String result = stringRedisTemplate.execute(setNxWithExpireTime, keys, expire.toString());
if (!"ok".equalsIgnoreCase(result)) {//不存在
return BaseResult.error("不允許重復(fù)提交,請(qǐng)稍后再試");
}
return joinPoint.proceed();
} catch (Throwable throwable) {
throw new RuntimeException(throwable.getMessage());
}
}
/**
* lua腳本
*/
private RedisScript<String> setNxWithExpireTime = new DefaultRedisScript<>(
"return redis.call('set', KEYS[1], 1, 'ex', ARGV[1], 'nx');",
String.class
);
/**
* 獲取唯一標(biāo)識(shí)key
*
* @param methodSignature
* @param args
* @return
*/
private String getLockUniqueKey(MethodSignature methodSignature, Object[] args) throws NoSuchAlgorithmException {
//請(qǐng)求uri, 獲取類名稱,方法名稱
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
// HttpServletResponse responese = servletRequestAttributes.getResponse();
//獲取用戶信息
String userMsg = SecurityUtils.getUsername(); //獲取登錄用戶名稱
//1.判斷用戶是否登錄
if (StringUtils.isEmpty(userMsg)) { //未登錄用戶獲取真實(shí)ip
userMsg = IpUtils.getIpAddr(request);
}
String hash = "";
List list = new ArrayList();
if (args.length > 0) {
String[] parameterNames = methodSignature.getParameterNames();
for (int i = 0; i < parameterNames.length; i++) {
Object obj = args[i];
list.add(obj);
}
String param = JSONObject.toJSONString(list);
hash = MyMD5Util.getMD5(param);
}
//項(xiàng)目名稱 + 環(huán)境編碼 + 獲取類名稱 + 加密參數(shù)
String key = "lock:" + springApplicationName + ":" + springProfilesActive + ":" + userMsg + ":" + request.getRequestURI();
if (StringUtils.isNotEmpty(key)) {
key = key + ":" + hash;
}
return key;
}
使用
@RedisLock
public void save(@RequestBody User user) {
}
總結(jié)
到此這篇關(guān)于java開(kāi)發(fā)中防止重復(fù)提交的幾種解決方案的文章就介紹到這了,更多相關(guān)java防止重復(fù)提交內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- java如何防止表單重復(fù)提交的注解@RepeatSubmit
- java后端如何實(shí)現(xiàn)防止接口重復(fù)提交
- Java使用注解實(shí)現(xiàn)防止重復(fù)提交實(shí)例
- java后臺(tái)防止表單重復(fù)提交方法詳解
- Java防止頻繁請(qǐng)求、重復(fù)提交的操作代碼(后端防抖操作)
- Java后端限制頻繁請(qǐng)求和重復(fù)提交的實(shí)現(xiàn)
- Java中防止數(shù)據(jù)重復(fù)提交超簡(jiǎn)單的6種方法
- Java結(jié)合redis實(shí)現(xiàn)接口防重復(fù)提交
- Java表單重復(fù)提交的避免方法
- JAVA防止重復(fù)提交Web表單的方法
- Java防止重復(fù)提交訂單的實(shí)現(xiàn)示例
相關(guān)文章
Java 獲取網(wǎng)絡(luò)302重定向URL的方法
在本篇文章里小編給大家整理的是關(guān)于Java 獲取網(wǎng)絡(luò)302重定向URL的方法以及相關(guān)知識(shí)點(diǎn),有興趣的朋友們參考下。2019-08-08
Java數(shù)據(jù)結(jié)構(gòu)之棧與隊(duì)列實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于Java數(shù)據(jù)結(jié)構(gòu)之棧與隊(duì)列的相關(guān)資料,算是作為用java描述數(shù)據(jù)結(jié)構(gòu)的一個(gè)開(kāi)始,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2021-11-11
詳解Spring?中?Bean?對(duì)象的存儲(chǔ)和取出
由于?Spring?擁有對(duì)象的管理權(quán),所以我們也需要擁有較為高效的對(duì)象存儲(chǔ)和取出的手段,下面我們來(lái)分別總結(jié)一下,對(duì)Spring?中?Bean?對(duì)象的存儲(chǔ)和取出知識(shí)感興趣的朋友跟隨小編一起看看吧2022-11-11
SpringBoot3集成WebSocket的全過(guò)程
WebSocket通過(guò)一個(gè)TCP連接在客戶端和服務(wù)器之間建立一個(gè)全雙工、雙向的通信通道,使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡(jiǎn)單,本文給大家介紹了SpringBoot3集成WebSocket的全過(guò)程,并有相關(guān)的代碼示例供大家參考,需要的朋友可以參考下2024-05-05
Spring中的注解之@Override和@Autowired
看別人寫的代碼,經(jīng)常會(huì)用到 @Override 和 @Autowired 這兩個(gè)注解.這邊總結(jié)一下這兩個(gè)注解的作用,對(duì)正在學(xué)習(xí)java的小伙伴們有很好地幫助,需要的朋友可以參考下2021-05-05
maven中deploy命令報(bào)401錯(cuò)誤的原因及解決方案
在mac版idea使用過(guò)程中有時(shí)候會(huì)出現(xiàn)deploy時(shí)候報(bào)401錯(cuò)誤,所以本文給大家介紹了maven 中deploy命令報(bào)401錯(cuò)誤的原因及解決方案,文章通過(guò)圖文結(jié)合的方式講解的非常詳細(xì),需要的朋友可以參考下2024-05-05

