一口氣說出8種冪等性解決重復(fù)提交的方案瓦侮,面試官懵了涵卵!(附代碼)

1.什么是冪等

在我們編程中常見冪等?

1)select查詢天然冪等 ??

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

3)update直接更新某個值的,冪等?

4)update更新累加操作的,非冪等?

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

2.產(chǎn)生原因

由于重復(fù)點擊或者網(wǎng)絡(luò)重發(fā) ?eg: ??

1)點擊提交按鈕兩次;?

2)點擊刷新按鈕;?

3)使用瀏覽器后退按鈕重復(fù)之前的操作,導(dǎo)致重復(fù)提交表單;?

4)使用瀏覽器歷史記錄重復(fù)提交表單;?

5)瀏覽器重復(fù)的HTTP請;?

6)nginx重發(fā)等情況;?

7)分布式RPC的try重發(fā)等;

3.解決方案

在提交后執(zhí)行頁面重定向击困,這就是所謂的Post-Redirect-Get (PRG)模式涎劈。

簡言之,當(dāng)用戶提交了表單后阅茶,你去執(zhí)行一個客戶端的重定向蛛枚,轉(zhuǎn)到提交成功信息頁面。

這能避免用戶按F5導(dǎo)致的重復(fù)提交脸哀,而其也不會出現(xiàn)瀏覽器表單重復(fù)提交的警告蹦浦,也能消除按瀏覽器前進和后退按導(dǎo)致的同樣問題。

在服務(wù)器端撞蜂,生成一個唯一的標(biāo)識符盲镶,將它存入session,同時將它寫入表單的隱藏字段中蝌诡,然后將表單頁面發(fā)給瀏覽器溉贿,用戶錄入信息后點擊提交,在服務(wù)器端浦旱,獲取表單中隱藏字段的值顽照,與session中的唯一標(biāo)識符比較,相等說明是首次提交闽寡,就處理本次請求代兵,然后將session中的唯一標(biāo)識符移除;不相等說明是重復(fù)提交爷狈,就不再處理植影。

比較復(fù)雜? 不適合移動端APP的應(yīng)用 這里不詳解

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

這種在大數(shù)據(jù)量和高并發(fā)下效率依賴數(shù)據(jù)庫硬件能力,可針對非核心業(yè)務(wù)

使用select ... for update? ,這種和 synchronized?

鎖住先查再insert or update一樣,但要避免死鎖,效率也較差?

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

原理:使用了 ConcurrentHashMap 并發(fā)容器 putIfAbsent 方法,和 ScheduledThreadPoolExecutor 定時任務(wù),也可以使用guava cache的機制, gauva中有配有緩存的有效時間 也是可以的key的生成 Content-MD5 Content-MD5 是指 Body 的 MD5 值,只有當(dāng) Body 非Form表單時才計算MD5涎永,計算方式直接將參數(shù)和參數(shù)名稱統(tǒng)一加密MD5思币。

MD5在一定范圍類認為是唯一的,近似唯一羡微,當(dāng)然在低并發(fā)的情況下足夠了 谷饿。

當(dāng)然本地鎖只適用于單機部署的應(yīng)用。

①配置注解

importjava.lang.annotation.*;

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

@Documented

public@interfaceResubmit {

/**

* 延時時間 在延時多久后可以再次提交

*

*@returnTime unit is one second

*/

intdelaySeconds()default20;

}

②實例化鎖

importcom.google.common.cache.Cache;

importcom.google.common.cache.CacheBuilder;

importlombok.extern.slf4j.Slf4j;

importorg.apache.commons.codec.digest.DigestUtils;

importjava.util.Objects;

importjava.util.concurrent.ConcurrentHashMap;

importjava.util.concurrent.ScheduledThreadPoolExecutor;

importjava.util.concurrent.ThreadPoolExecutor;

importjava.util.concurrent.TimeUnit;

/**

*@authorlijing

* 重復(fù)提交鎖

*/

@Slf4j

publicfinalclassResubmitLock{

privatestaticfinalConcurrentHashMapLOCK_CACHE =newConcurrentHashMap<>(200);

privatestaticfinalScheduledThreadPoolExecutor EXECUTOR =newScheduledThreadPoolExecutor(5,newThreadPoolExecutor.DiscardPolicy());

// private static final CacheCACHES = CacheBuilder.newBuilder()

// 最大緩存 100 個

// .maximumSize(1000)

// 設(shè)置寫緩存后 5 秒鐘過期

// .expireAfterWrite(5, TimeUnit.SECONDS)

// .build();

privateResubmitLock(){

}

/**

* 靜態(tài)內(nèi)部類 單例模式

*

*@return

*/

privatestaticclassSingletonInstance{

privatestaticfinalResubmitLock INSTANCE =newResubmitLock();

}

publicstaticResubmitLockgetInstance(){

returnSingletonInstance.INSTANCE;

}

publicstaticStringhandleKey(String param){

returnDigestUtils.md5Hex(param ==null?"": param);

}

/**

* 加鎖 putIfAbsent 是原子操作保證線程安全

*

*@paramkey 對應(yīng)的key

*@paramvalue

*@return

*/

publicbooleanlock(finalString key, Object value){

returnObjects.isNull(LOCK_CACHE.putIfAbsent(key, value));

}

/**

* 延時釋放鎖 用以控制短時間內(nèi)的重復(fù)提交

*

*@paramlock 是否需要解鎖

*@paramkey 對應(yīng)的key

*@paramdelaySeconds 延時時間

*/

publicvoidunLock(finalbooleanlock,finalString key,finalintdelaySeconds){

if(lock) {

EXECUTOR.schedule(() -> {

LOCK_CACHE.remove(key);

}, delaySeconds, TimeUnit.SECONDS);

}

}

}

③AOP 切面

importcom.alibaba.fastjson.JSONObject;

importcom.cn.xxx.common.annotation.Resubmit;

importcom.cn.xxx.common.annotation.impl.ResubmitLock;

importcom.cn.xxx.common.dto.RequestDTO;

importcom.cn.xxx.common.dto.ResponseDTO;

importcom.cn.xxx.common.enums.ResponseCode;

importlombok.extern.log4j.Log4j;

importorg.aspectj.lang.ProceedingJoinPoint;

importorg.aspectj.lang.annotation.Around;

importorg.aspectj.lang.annotation.Aspect;

importorg.aspectj.lang.reflect.MethodSignature;

importorg.springframework.stereotype.Component;

importjava.lang.reflect.Method;

/**

*@ClassNameRequestDataAspect

*@Description數(shù)據(jù)重復(fù)提交校驗

*@Authorlijing

*@Date2019/05/16 17:05

**/

@Log4j

@Aspect

@Component

publicclassResubmitDataAspect{

privatefinalstaticString DATA ="data";

privatefinalstaticObject PRESENT =newObject();

@Around("@annotation(com.cn.xxx.common.annotation.Resubmit)")

publicObjecthandleResubmit(ProceedingJoinPoint joinPoint)throwsThrowable{

Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();

//獲取注解信息

Resubmit annotation = method.getAnnotation(Resubmit.class);

intdelaySeconds = annotation.delaySeconds();

Object[] pointArgs = joinPoint.getArgs();

String key ="";

//獲取第一個參數(shù)

Object firstParam = pointArgs[0];

if(firstParaminstanceofRequestDTO) {

//解析參數(shù)

JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());

JSONObject data = JSONObject.parseObject(requestDTO.getString(DATA));

if(data !=null) {

StringBuffer sb =newStringBuffer();

data.forEach((k, v) -> {

sb.append(v);

});

//生成加密參數(shù) 使用了content_MD5的加密方式

key = ResubmitLock.handleKey(sb.toString());

}

}

//執(zhí)行鎖

booleanlock =false;

try{

//設(shè)置解鎖key

lock = ResubmitLock.getInstance().lock(key, PRESENT);

if(lock) {

//放行

returnjoinPoint.proceed();

}else{

//響應(yīng)重復(fù)提交異常

returnnewResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);

}

}finally{

//設(shè)置解鎖key和解鎖時間

ResubmitLock.getInstance().unLock(lock, key, delaySeconds);

}

}

}

④注解使用案例

@ApiOperation(value ="保存我的帖子接口", notes ="保存我的帖子接口")

@PostMapping("/posts/save")

@Resubmit(delaySeconds =10)

public ResponseDTOsaveBbsPosts(@RequestBody@ValidatedRequestDTOrequestDto) {

returnbbsPostsBizService.saveBbsPosts(requestDto);

}

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

當(dāng)然也可以使用 ?一些其他簽名校驗 ?在某一次提交時先 生成固定簽名 ?提交到后端 根據(jù)后端解析統(tǒng)一的簽名作為 每次提交的驗證token 去緩存中處理即可博投。

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

org.springframework.bootgroupId>

spring-boot-starter-webartifactId>

dependency>

org.springframework.bootgroupId>

spring-boot-starter-aopartifactId>

dependency>

org.springframework.bootgroupId>

spring-boot-starter-data-redisartifactId>

dependency>

dependencies>

屬性配置 在 application.properites 資源文件中添加 redis 相關(guān)的配置項:

spring.redis.host=localhost

spring.redis.port=6379

spring.redis.password=123456

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

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

packagecom.battcn.interceptor;

importcom.battcn.annotation.CacheLock;

importcom.battcn.utils.RedisLockHelper;

importorg.aspectj.lang.ProceedingJoinPoint;

importorg.aspectj.lang.annotation.Around;

importorg.aspectj.lang.annotation.Aspect;

importorg.aspectj.lang.reflect.MethodSignature;

importorg.springframework.beans.factory.annotation.Autowired;

importorg.springframework.context.annotation.Configuration;

importorg.springframework.util.StringUtils;

importjava.lang.reflect.Method;

importjava.util.UUID;

/**

* redis 方案

*

*@authorLevin

*@since2018/6/12 0012

*/

@Aspect

@Configuration

publicclassLockMethodInterceptor{

@Autowired

publicLockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator){

this.redisLockHelper = redisLockHelper;

this.cacheKeyGenerator = cacheKeyGenerator;

}

privatefinalRedisLockHelper redisLockHelper;

privatefinalCacheKeyGenerator cacheKeyGenerator;

@Around("execution(public * *(..)) && @annotation(com.battcn.annotation.CacheLock)")

publicObjectinterceptor(ProceedingJoinPoint pjp){

MethodSignature signature = (MethodSignature) pjp.getSignature();

Method method = signature.getMethod();

CacheLock lock = method.getAnnotation(CacheLock.class);

if(StringUtils.isEmpty(lock.prefix())) {

thrownewRuntimeException("lock key don't null...");

}

finalString lockKey = cacheKeyGenerator.getLockKey(pjp);

String value = UUID.randomUUID().toString();

try{

// 假設(shè)上鎖成功翅睛,但是設(shè)置過期時間失效,以后拿到的都是 false

finalbooleansuccess = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());

if(!success) {

thrownewRuntimeException("重復(fù)提交");

}

try{

returnpjp.proceed();

}catch(Throwable throwable) {

thrownewRuntimeException("系統(tǒng)異常");

}

}finally{

// TODO 如果演示的話需要注釋該代碼;實際應(yīng)該放開

redisLockHelper.unlock(lockKey, value);

}

}

}

RedisLockHelper 通過封裝成 API 方式調(diào)用黑竞,靈活度更加高

packagecom.battcn.utils;

importorg.springframework.boot.autoconfigure.AutoConfigureAfter;

importorg.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;

importorg.springframework.context.annotation.Configuration;

importorg.springframework.data.redis.connection.RedisStringCommands;

importorg.springframework.data.redis.core.RedisCallback;

importorg.springframework.data.redis.core.StringRedisTemplate;

importorg.springframework.data.redis.core.types.Expiration;

importorg.springframework.util.StringUtils;

importjava.util.concurrent.Executors;

importjava.util.concurrent.ScheduledExecutorService;

importjava.util.concurrent.TimeUnit;

importjava.util.regex.Pattern;

/**

* 需要定義成 Bean

*

*@authorLevin

*@since2018/6/15 0015

*/

@Configuration

@AutoConfigureAfter(RedisAutoConfiguration.class)

publicclassRedisLockHelper{

privatestaticfinalString DELIMITER ="|";

/**

* 如果要求比較高可以通過注入的方式分配

*/

privatestaticfinalScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);

privatefinalStringRedisTemplate stringRedisTemplate;

publicRedisLockHelper(StringRedisTemplate stringRedisTemplate){

this.stringRedisTemplate = stringRedisTemplate;

}

/**

* 獲取鎖(存在死鎖風(fēng)險)

*

*@paramlockKey lockKey

*@paramvalue value

*@paramtime 超時時間

*@paramunit 過期單位

*@returntrue or false

*/

publicbooleantryLock(finalString lockKey,finalString value,finallongtime,finalTimeUnit unit){

returnstringRedisTemplate.execute((RedisCallback) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));

}

/**

* 獲取鎖

*

*@paramlockKey lockKey

*@paramuuid UUID

*@paramtimeout 超時時間

*@paramunit 過期單位

*@returntrue or false

*/

publicbooleanlock(String lockKey,finalString uuid,longtimeout,finalTimeUnit unit){

finallongmilliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();

booleansuccess = 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);

finalString[] oldValues = oldVal.split(Pattern.quote(DELIMITER));

if(Long.parseLong(oldValues[0]) +1<= System.currentTimeMillis()) {

returntrue;

}

}

returnsuccess;

}

/**

*@seeRedis Documentation: SET

*/

publicvoidunlock(String lockKey, String value){

unlock(lockKey, value,0, TimeUnit.MILLISECONDS);

}

/**

* 延遲unlock

*

*@paramlockKey key

*@paramuuid client(最好是唯一鍵的)

*@paramdelayTime 延遲時間

*@paramunit 時間單位

*/

publicvoidunlock(finalString lockKey,finalString uuid,longdelayTime, TimeUnit unit){

if(StringUtils.isEmpty(lockKey)) {

return;

}

if(delayTime <=0) {

doUnlock(lockKey, uuid);

}else{

EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);

}

}

/**

*@paramlockKey key

*@paramuuid client(最好是唯一鍵的)

*/

privatevoiddoUnlock(finalString lockKey,finalString uuid){

String val = stringRedisTemplate.opsForValue().get(lockKey);

finalString[] 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/


END

本文發(fā)于 微星公眾號「程序員的成長之路」捕发,回復(fù)「1024」你懂得,給個贊唄摊溶。

回復(fù) [ 256 ] Java 程序員成長規(guī)劃

回復(fù) [ 777 ] 接私活的七大平臺利器

回復(fù) [ 2048 ] 免費領(lǐng)取C/C++爬骤,Linux,Python莫换,Java霞玄,PHP,人工智能拉岁,單片機坷剧,樹莓派,等 5T 學(xué)習(xí)資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喊暖,一起剝皮案震驚了整個濱河市惫企,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖狞尔,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丛版,死亡現(xiàn)場離奇詭異,居然都是意外死亡偏序,警方通過查閱死者的電腦和手機页畦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來研儒,“玉大人豫缨,你說我怎么就攤上這事《硕洌” “怎么了好芭?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長冲呢。 經(jīng)常有香客問我舍败,道長,這世上最難降的妖魔是什么碗硬? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任瓤湘,我火速辦了婚禮,結(jié)果婚禮上恩尾,老公的妹妹穿的比我還像新娘弛说。我一直安慰自己,他們只是感情好翰意,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布木人。 她就那樣靜靜地躺著,像睡著了一般冀偶。 火紅的嫁衣襯著肌膚如雪醒第。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天进鸠,我揣著相機與錄音稠曼,去河邊找鬼。 笑死客年,一個胖子當(dāng)著我的面吹牛霞幅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播量瓜,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼司恳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绍傲?” 一聲冷哼從身側(cè)響起扔傅,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后猎塞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體试读,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年邢享,在試婚紗的時候發(fā)現(xiàn)自己被綠了鹏往。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡骇塘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出韩容,到底是詐尸還是另有隱情款违,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布群凶,位于F島的核電站插爹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏请梢。R本人自食惡果不足惜赠尾,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望毅弧。 院中可真熱鬧气嫁,春花似錦、人聲如沸够坐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽元咙。三九已至梯影,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間庶香,已是汗流浹背甲棍。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赶掖,地道東北人感猛。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像倘零,于是被迫代替她去往敵國和親唱遭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355