日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

8 种方案解决重复提交问题!你选择哪一种呀?

發布時間:2025/3/21 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 8 种方案解决重复提交问题!你选择哪一种呀? 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1.什么是冪等

在我們編程中常見冪等

  • select查詢天然冪等

  • delete刪除也是冪等,刪除同一個多次效果一樣

  • update直接更新某個值的,冪等

  • update更新累加操作的,非冪等

  • insert非冪等操作,每次新增一條

2.產生原因

由于重復點擊或者網絡重發 eg:

  • 點擊提交按鈕兩次;

  • 點擊刷新按鈕;

  • 使用瀏覽器后退按鈕重復之前的操作,導致重復提交表單;

  • 使用瀏覽器歷史記錄重復提交表單;

  • 瀏覽器重復的HTTP請;

  • nginx重發等情況;

  • 分布式RPC的try重發等;

3.解決方案

1)前端js提交禁止按鈕可以用一些js組件

2)使用Post/Redirect/Get模式

在提交后執行頁面重定向,這就是所謂的Post-Redirect-Get (PRG)模式。簡言之,當用戶提交了表單后,你去執行一個客戶端的重定向,轉到提交成功信息頁面。這能避免用戶按F5導致的重復提交,而其也不會出現瀏覽器表單重復提交的警告,也能消除按瀏覽器前進和后退按導致的同樣問題。

3)在session中存放一個特殊標志

在服務器端,生成一個唯一的標識符,將它存入session,同時將它寫入表單的隱藏字段中,然后將表單頁面發給瀏覽器,用戶錄入信息后點擊提交,在服務器端,獲取表單中隱藏字段的值,與session中的唯一標識符比較,相等說明是首次提交,就處理本次請求,然后將session中的唯一標識符移除;不相等說明是重復提交,就不再處理。

4)其他借助使用header頭設置緩存控制頭Cache-control等方式

比較復雜 不適合移動端APP的應用 這里不詳解

5)借助數據庫

insert使用唯一索引 update使用 樂觀鎖 version版本法

這種在大數據量和高并發下效率依賴數據庫硬件能力,可針對非核心業務

6)借助悲觀鎖

使用select … for update ,這種和 synchronized 鎖住先查再insert or update一樣,但要避免死鎖,效率也較差

針對單體 請求并發不大 可以推薦使用

7)借助本地鎖(本文重點)

原理:

使用了 ConcurrentHashMap 并發容器 putIfAbsent 方法,和 ScheduledThreadPoolExecutor 定時任務,也可以使用guava cache的機制, gauva中有配有緩存的有效時間也是可以的key的生成

Content-MD5

Content-MD5 是指 Body 的 MD5 值,只有當 Body 非Form表單時才計算MD5,計算方式直接將參數和參數名稱統一加密MD5

MD5在一定范圍類認為是唯一的 近似唯一 當然在低并發的情況下足夠了

本地鎖只適用于單機部署的應用.

①配置注解

import java.lang.annotation.*;@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Resubmit {/*** 延時時間 在延時多久后可以再次提交** @return Time unit is one second*/int delaySeconds() default 20; }

②實例化鎖

import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils;import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit;/*** @author lijing* 重復提交鎖*/ @Slf4j public final class ResubmitLock {private static final ConcurrentHashMap<String, Object> LOCK_CACHE = new ConcurrentHashMap<>(200);private static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5, new ThreadPoolExecutor.DiscardPolicy());// private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()// 最大緩存 100 個// .maximumSize(1000)// 設置寫緩存后 5 秒鐘過期// .expireAfterWrite(5, TimeUnit.SECONDS)// .build();private ResubmitLock() {}/*** 靜態內部類 單例模式** @return*/private static class SingletonInstance {private static final ResubmitLock INSTANCE = new ResubmitLock();}public static ResubmitLock getInstance() {return SingletonInstance.INSTANCE;}public static String handleKey(String param) {return DigestUtils.md5Hex(param == null ? "" : param);}/*** 加鎖 putIfAbsent 是原子操作保證線程安全** @param key 對應的key* @param value* @return*/public boolean lock(final String key, Object value) {return Objects.isNull(LOCK_CACHE.putIfAbsent(key, value));}/*** 延時釋放鎖 用以控制短時間內的重復提交** @param lock 是否需要解鎖* @param key 對應的key* @param delaySeconds 延時時間*/public void unLock(final boolean lock, final String key, final int delaySeconds) {if (lock) {EXECUTOR.schedule(() -> {LOCK_CACHE.remove(key);}, delaySeconds, TimeUnit.SECONDS);}} }

③AOP 切面

import com.alibaba.fastjson.JSONObject; import com.cn.xxx.common.annotation.Resubmit; import com.cn.xxx.common.annotation.impl.ResubmitLock; import com.cn.xxx.common.dto.RequestDTO; import com.cn.xxx.common.dto.ResponseDTO; import com.cn.xxx.common.enums.ResponseCode; import lombok.extern.log4j.Log4j; 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.stereotype.Component;import java.lang.reflect.Method;/*** @ClassName RequestDataAspect* @Description 數據重復提交校驗* @Author lijing* @Date 2019/05/16 17:05**/ @Log4j @Aspect @Component public class ResubmitDataAspect {private final static String DATA = "data";private final static Object PRESENT = new Object();@Around("@annotation(com.cn.xxx.common.annotation.Resubmit)")public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable {Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();//獲取注解信息Resubmit annotation = method.getAnnotation(Resubmit.class);int delaySeconds = annotation.delaySeconds();Object[] pointArgs = joinPoint.getArgs();String key = "";//獲取第一個參數Object firstParam = pointArgs[0];if (firstParam instanceof RequestDTO) {//解析參數JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());JSONObject data = JSONObject.parseObject(requestDTO.getString(DATA));if (data != null) {StringBuffer sb = new StringBuffer();data.forEach((k, v) -> {sb.append(v);});//生成加密參數 使用了content_MD5的加密方式key = ResubmitLock.handleKey(sb.toString());}}//執行鎖boolean lock = false;try {//設置解鎖keylock = ResubmitLock.getInstance().lock(key, PRESENT);if (lock) {//放行return joinPoint.proceed();} else {//響應重復提交異常return new ResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);}} finally {//設置解鎖key和解鎖時間ResubmitLock.getInstance().unLock(lock, key, delaySeconds);}} }

④注解使用案例

@ApiOperation(value = "保存我的帖子接口", notes = "保存我的帖子接口")@PostMapping("/posts/save")@Resubmit(delaySeconds = 10)public ResponseDTO<BaseResponseDataDTO> saveBbsPosts(@RequestBody @Validated RequestDTO<BbsPostsRequestDTO> requestDto) {return bbsPostsBizService.saveBbsPosts(requestDto);}

以上就是本地鎖的方式進行的冪等提交 使用了Content-MD5 進行加密 只要參數不變,參數加密 密值不變,key存在就阻止提交

當然也可以使用 一些其他簽名校驗 在某一次提交時先 生成固定簽名 提交到后端 根據后端解析統一的簽名作為 每次提交的驗證token 去緩存中處理即可.

8)借助分布式redis鎖 (參考其他)

在 pom.xml 中添加上 starter-web、starter-aop、starter-data-redis?的依賴即可

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency> </dependencies>

屬性配置 在 application.properites 資源文件中添加?redis?相關的配置項

spring.redis.host=localhost spring.redis.port=6379 spring.redis.password=123456

主要實現方式:

熟悉?Redis?的朋友都知道它是線程安全的,我們利用它的特性可以很輕松的實現一個分布式鎖,如 opsForValue().setIfAbsent(key,value)它的作用就是如果緩存中沒有當前 Key 則進行緩存同時返回 true 反之亦然;

當緩存后給 key 在設置個過期時間,防止因為系統崩潰而導致鎖遲遲不釋放形成死鎖;那么我們是不是可以這樣認為當返回 true 我們認為它獲取到鎖了,在鎖未釋放的時候我們進行異常的拋出…

package com.battcn.interceptor;import com.battcn.annotation.CacheLock; import com.battcn.utils.RedisLockHelper; 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.util.StringUtils;import java.lang.reflect.Method; import java.util.UUID;/*** redis 方案** @author Levin* @since 2018/6/12 0012*/ @Aspect @Configuration public class LockMethodInterceptor {@Autowiredpublic LockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator) {this.redisLockHelper = redisLockHelper;this.cacheKeyGenerator = cacheKeyGenerator;}private final RedisLockHelper redisLockHelper;private final CacheKeyGenerator cacheKeyGenerator;@Around("execution(public * *(..)) && @annotation(com.battcn.annotation.CacheLock)")public Object interceptor(ProceedingJoinPoint pjp) {MethodSignature signature = (MethodSignature) pjp.getSignature();Method method = signature.getMethod();CacheLock lock = method.getAnnotation(CacheLock.class);if (StringUtils.isEmpty(lock.prefix())) {throw new RuntimeException("lock key don't null...");}final String lockKey = cacheKeyGenerator.getLockKey(pjp);String value = UUID.randomUUID().toString();try {// 假設上鎖成功,但是設置過期時間失效,以后拿到的都是 falsefinal boolean success = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());if (!success) {throw new RuntimeException("重復提交");}try {return pjp.proceed();} catch (Throwable throwable) {throw new RuntimeException("系統異常");}} finally {// TODO 如果演示的話需要注釋該代碼;實際應該放開redisLockHelper.unlock(lockKey, value);}} }

RedisLockHelper 通過封裝成 API 方式調用,靈活度更加高

package com.battcn.utils;import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; 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.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern;/*** 需要定義成 Bean** @author Levin* @since 2018/6/15 0015*/ @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) public class RedisLockHelper {private static final String DELIMITER = "|";/*** 如果要求比較高可以通過注入的方式分配*/private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);private final StringRedisTemplate stringRedisTemplate;public RedisLockHelper(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}/*** 獲取鎖(存在死鎖風險)** @param lockKey lockKey* @param value value* @param time 超時時間* @param unit 過期單位* @return true or false*/public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit) {return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));}/*** 獲取鎖** @param lockKey lockKey* @param uuid UUID* @param timeout 超時時間* @param unit 過期單位* @return true or false*/public boolean lock(String lockKey, final String uuid, long timeout, final TimeUnit unit) {final long milliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);if (success) {stringRedisTemplate.expire(lockKey, timeout, TimeUnit.SECONDS);} else {String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));if (Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()) {return true;}}return success;}/*** @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a>*/public void unlock(String lockKey, String value) {unlock(lockKey, value, 0, TimeUnit.MILLISECONDS);}/*** 延遲unlock** @param lockKey key* @param uuid client(最好是唯一鍵的)* @param delayTime 延遲時間* @param unit 時間單位*/public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {if (StringUtils.isEmpty(lockKey)) {return;}if (delayTime <= 0) {doUnlock(lockKey, uuid);} else {EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);}}/*** @param lockKey key* @param uuid client(最好是唯一鍵的)*/private void doUnlock(final String lockKey, final String uuid) {String val = stringRedisTemplate.opsForValue().get(lockKey);final String[] values = val.split(Pattern.quote(DELIMITER));if (values.length <= 0) {return;}if (uuid.equals(values[1])) {stringRedisTemplate.delete(lockKey);}}}

redis的提交參照

https://blog.battcn.com/2018/06/13/springboot/v2-cache-redislock/

總結

以上是生活随笔為你收集整理的8 种方案解决重复提交问题!你选择哪一种呀?的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 国产午夜精品理论片在线 | 91视频黄色 | 久久久久久久久一区 | 亚洲粉嫩| 国产成人无码久久久精品天美传媒 | 亚洲精品污一区二区三区 | 午夜精品一区二区三区三上悠亚 | 日韩一级淫片 | 麻豆一区二区三区 | 天堂网中文在线观看 | 国产精品成人自拍 | 在线观看成年人视频 | 午夜婷婷色 | 欧亚一区二区 | 乱精品一区字幕二区 | 精品久久影院 | 狠狠躁夜夜躁av无码中文幕 | 欧美日韩视频网站 | 中文字幕在线1 | 亚洲国产精品一区 | 黄色一区二区视频 | 国产视频一区二区三区四区 | 精品国产丝袜一区二区三区乱码 | 18欧美性xxxx极品hd | 色婷在线 | 丝袜中文字幕 | 放几个免费的毛片出来看 | 中文字幕免费观看视频 | 痴女扩张宫交脱垂重口小说 | 婷婷的五月 | 卡一卡二视频 | 欧美男女交配 | 91欧美视频| 欧美日韩高清不卡 | 欧美人妻精品一区二区三区 | 经典三级视频 | 亚洲av无码乱码国产麻豆 | 男女一级片 | 久久99久久精品 | 特级新鲜大片片 | 亚洲熟妇毛茸茸 | 午夜精品久久久久久久久久久久久蜜桃 | 欧美激情一区二区三区蜜桃视频 | 国产亚洲一区二区三区四区 | 亚洲免费大全 | 亚洲国产精品国自产拍av | 午夜激情在线视频 | 爱啪啪av| 国产精品久久久久一区二区 | 天天草天天射 | 久久亚洲精精品中文字幕早川悠里 | 91欧美日韩 | 日本三级午夜理伦三级三 | 草逼免费视频 | 日本公妇乱偷中文字幕 | 亚洲欧美日韩不卡 | 精品动漫一区 | 男男做爰猛烈叫床爽爽小说 | 少妇视频网站 | 精品午夜福利视频 | 一区久久 | 久久久久久久久久免费视频 | 自偷自拍亚洲 | 激情五月婷婷久久 | 精品国产欧美日韩 | 日本高清xxx | 东方成人av在线 | 加勒比综合网 | 91av影院 | 午夜激情亚洲 | 男男黄色片 | 亚洲青草视频 | 中文字幕一区二区三区四区五区 | 男女h网站 | 国产精品伦子伦免费视频 | 久久不卡日韩美女 | 激情五月色婷婷 | 欧美国产第一页 | 国产日皮视频 | 日韩欧美视频免费观看 | 日本特级淫片 | 激情网久久 | 日韩欧美亚洲一区二区 | 三女警花合力承欢猎艳都市h | 中日韩在线观看视频 | 久久午夜鲁丝 | www.youjizz日本 | 亚洲狼人在线 | 德国艳星videos极品hd | 成人在线免费小视频 | 欧美福利一区二区 | 婷婷爱五月 | 成年人的天堂 | 日韩免费av| 亚洲精品国产欧美 | 黄色特一级 | 久久久精彩视频 | 少妇熟女视频一区二区三区 | 夜夜爱av |