重復提交看似是一個小兒科的問題,但卻存在好幾種變種用法。在面試中回答的好,說不定會有意想不到的收獲!現把這 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,計算方式直接將參數和參數名稱統一加密MD5MD5在一定范圍類認為是唯一的 近似唯一 當然在低并發的情況下足夠了本地鎖只適用于單機部署的應用.①配置注解
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?LOCK_CACHE?=?new?ConcurrentHashMap<>(200);private?static?final?ScheduledThreadPoolExecutor?EXECUTOR?=?new?ScheduledThreadPoolExecutor(5,?new?ThreadPoolExecutor.DiscardPolicy());//?private?static?final?Cache?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?{
????????????//設置解鎖key
????????????lock?=?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?saveBbsPosts(@RequestBody?@Validated?RequestDTO?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.bootgroupId>
????????<artifactId>spring-boot-starter-webartifactId>
????dependency>
????<dependency>
????????<groupId>org.springframework.bootgroupId>
????????<artifactId>spring-boot-starter-aopartifactId>
????dependency>
????<dependency>
????????<groupId>org.springframework.bootgroupId>
????????<artifactId>spring-boot-starter-data-redisartifactId>
????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?{
????@Autowired
????public?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?{
????????????//?假設上鎖成功,但是設置過期時間失效,以后拿到的都是?false
????????????final?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?Redis?Documentation:?SET
?????*/
????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/