java開發(fā)中防止重復(fù)提交的幾種解決方案
一、產(chǎn)生原因
對(duì)于重復(fù)提交的問題,主要由于重復(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ù)提交的問題 主要涉及到時(shí) 冪等 問題,那么先說一下什么是冪等。
冪等:F(F(X)) = F(X)多次運(yùn)算結(jié)果一致;簡單點(diǎn)說就是對(duì)于完全相同的操作,操作一次與操作多次的結(jié)果是一樣的。
在開發(fā)中,我們都會(huì)涉及到對(duì)數(shù)據(jù)庫操作。例如:
select 查詢天然冪等
delete 刪除也是冪等,刪除同一個(gè)多次效果一樣
update 直接更新某個(gè)值(如:狀態(tài) 字段固定值),冪等
update 更新累加操作(如:商品數(shù)量 字段),非冪等
(可以采用簡單的樂觀鎖和悲觀鎖 個(gè)人更喜歡樂觀鎖。
樂觀鎖:數(shù)據(jù)庫表加version字段的方式;
悲觀鎖:用了 select…for update 的方式,* 要使用悲觀鎖,我們必須關(guān)閉mysql數(shù)據(jù)庫的自動(dòng)提交屬性。
這種在大數(shù)據(jù)量和高并發(fā)下效率依賴數(shù)據(jù)庫硬件能力,可針對(duì)并發(fā)量不高的非核心業(yè)務(wù);)
insert 非冪等操作,每次新增一條 重點(diǎn) (數(shù)據(jù)庫簡單方案:可采取數(shù)據(jù)庫唯一索引方式;這種在大數(shù)據(jù)量和高并發(fā)下效率依賴數(shù)據(jù)庫硬件能力,可針對(duì)并發(fā)量不高的非核心業(yè)務(wù);)
三、解決方案
1. 方案對(duì)比
序號(hào) | 前端/后端 | 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) | 代碼實(shí)現(xiàn) |
---|---|---|---|---|---|
1) | 前端 | 前端js提交后禁止按鈕,返回結(jié)果后解禁等 | 簡單 方便 | 只能控制頁面,通過工具可繞過不安全 | 略 |
2) | 后端 | 提交后重定向到其他頁面,防止用戶F5和瀏覽器前進(jìn)后退等重復(fù)提交問題 | 簡單 方便 | 體驗(yàn)不好,適用部分場(chǎng)景,若是遇到網(wǎng)絡(luò)問題 還會(huì)出現(xiàn) | 略 |
3) | 后端 | 在表單、session、token 放入唯一標(biāo)識(shí)符(如:UUID),每次操作時(shí),保存標(biāo)識(shí)一定時(shí)間后移除,保存期間有相同的標(biāo)識(shí)就不處理或提示 | 相對(duì)簡單 | 表單:有時(shí)需要前后端協(xié)商配合; session、token:加大服務(wù)性能開銷 | 略 |
4) | 后端 | ConcurrentHashMap 、LRUMap 、google Cache 都是采用唯一標(biāo)識(shí)(如:用戶ID+請(qǐng)求路徑+參數(shù)) | 相對(duì)簡單 | 適用于單機(jī)部署的應(yīng)用 | 見下 |
5) | 后端 | redis 是線程安全的,可以實(shí)現(xiàn)redis分布式鎖。設(shè)置唯一標(biāo)識(shí)(如:用戶ID+請(qǐng)求路徑+參數(shù))當(dāng)做key ,value值可以隨意(推薦設(shè)置成過期的時(shí)間點(diǎn)),在設(shè)置key的過期時(shí)間 | 單機(jī)、分布式、高并發(fā)都可以決絕 | 相對(duì)復(fù)雜需要部署維護(hù)redis | 見下 |
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ù)及過期日期 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開發(fā)中防止重復(fù)提交的幾種解決方案的文章就介紹到這了,更多相關(guān)java防止重復(fù)提交內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringCloud Bus消息總線的實(shí)現(xiàn)
消息總線是一種通信工具,可以在機(jī)器之間互相傳輸消息、文件等,這篇文章主要介紹了SpringCloud Bus消息總線的實(shí)現(xiàn),Spring cloud bus 通過輕量消息代理連接各個(gè)分布的節(jié)點(diǎn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05Mybatis實(shí)現(xiàn)數(shù)據(jù)的增刪改查實(shí)例(CRUD)
本篇文章主要介紹了Mybatis實(shí)現(xiàn)數(shù)據(jù)的增刪改查實(shí)例(CRUD),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05java對(duì)接微信小程序詳細(xì)流程(登錄&獲取用戶信息)
這篇文章主要給大家介紹了關(guān)于java對(duì)接微信小程序(登錄&獲取用戶信息)的相關(guān)資料,我們?cè)陂_發(fā)微信小程序時(shí)經(jīng)常需要獲取用戶微信用戶名以及頭像信息,微信提供了專門的接口API用于返回這些信息,需要的朋友可以參考下2023-08-08使用多個(gè)servlet時(shí)Spring security需要指明路由匹配策略問題
這篇文章主要介紹了使用多個(gè)servlet時(shí)Spring security需要指明路由匹配策略問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08Java Web項(xiàng)目中驗(yàn)證碼功能的制作攻略
使用servlet制作驗(yàn)證碼中最關(guān)鍵的部分是緩存的使用,驗(yàn)證session中的字符串,接下來我們就來看一下Java Web項(xiàng)目中驗(yàn)證碼功能的制作攻略2016-05-05SpringBoot集成Mybatis實(shí)現(xiàn)對(duì)多數(shù)據(jù)源訪問原理
本文主要分析討論在SpringBoot應(yīng)用中我們?cè)撊绾闻渲肧qlSessionFactoryBean對(duì)象,進(jìn)而實(shí)現(xiàn)對(duì)多個(gè)不同的數(shù)據(jù)源的操縱,文章通過代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2023-11-11