一、什么是冪等踪旷?
看一下維基百科怎么說(shuō)的:
冪等性:多次調(diào)用方法或者接口不會(huì)改變業(yè)務(wù)狀態(tài)植捎,可以保證重復(fù)調(diào)用的結(jié)果和單次調(diào)用的結(jié)果一致衙解。
二、使用冪等的場(chǎng)景
1焰枢、前端重復(fù)提交
用戶(hù)注冊(cè)蚓峦,用戶(hù)創(chuàng)建商品等操作,前端都會(huì)提交一些數(shù)據(jù)給后臺(tái)服務(wù)济锄,后臺(tái)需要根據(jù)用戶(hù)提交的數(shù)據(jù)在數(shù)據(jù)庫(kù)中創(chuàng)建記錄暑椰。如果用戶(hù)不小心多點(diǎn)了幾次,后端收到了好幾次提交荐绝,這時(shí)就會(huì)在數(shù)據(jù)庫(kù)中重復(fù)創(chuàng)建了多條記錄一汽。這就是接口沒(méi)有冪等性帶來(lái)的 bug。
2低滩、接口超時(shí)重試
對(duì)于給第三方調(diào)用的接口召夹,有可能會(huì)因?yàn)榫W(wǎng)絡(luò)原因而調(diào)用失敗,這時(shí)恕沫,一般在設(shè)計(jì)的時(shí)候會(huì)對(duì)接口調(diào)用加上失敗重試的機(jī)制监憎。如果第一次調(diào)用已經(jīng)執(zhí)行了一半時(shí),發(fā)生了網(wǎng)絡(luò)異常婶溯。這時(shí)再次調(diào)用時(shí)就會(huì)因?yàn)榕K數(shù)據(jù)的存在而出現(xiàn)調(diào)用異常鲸阔。
3偷霉、消息重復(fù)消費(fèi)
在使用消息中間件來(lái)處理消息隊(duì)列,且手動(dòng) ack 確認(rèn)消息被正常消費(fèi)時(shí)隶债。如果消費(fèi)者突然斷開(kāi)連接腾它,那么已經(jīng)執(zhí)行了一半的消息會(huì)重新放回隊(duì)列。
當(dāng)消息被其他消費(fèi)者重新消費(fèi)時(shí)死讹,如果沒(méi)有冪等性瞒滴,就會(huì)導(dǎo)致消息重復(fù)消費(fèi)時(shí)結(jié)果異常,如數(shù)據(jù)庫(kù)重復(fù)數(shù)據(jù)赞警,數(shù)據(jù)庫(kù)數(shù)據(jù)沖突妓忍,資源重復(fù)等。
三愧旦、解決方案
1世剖、token 機(jī)制實(shí)現(xiàn)
通過(guò)token 機(jī)制實(shí)現(xiàn)接口的冪等性,這是一種比較通用性的實(shí)現(xiàn)方法。
示意圖如下:
具體流程步驟:
- 客戶(hù)端會(huì)先發(fā)送一個(gè)請(qǐng)求去獲取 token笤虫,服務(wù)端會(huì)生成一個(gè)全局唯一的 ID 作為 token 保存在 redis 中旁瘫,同時(shí)把這個(gè) ID 返回給客戶(hù)端
- 客戶(hù)端第二次調(diào)用業(yè)務(wù)請(qǐng)求的時(shí)候必須攜帶這個(gè) token
- 服務(wù)端會(huì)校驗(yàn)這個(gè) token,如果校驗(yàn)成功琼蚯,則執(zhí)行業(yè)務(wù)酬凳,并刪除 redis 中的 token
- 如果校驗(yàn)失敗,說(shuō)明 redis 中已經(jīng)沒(méi)有對(duì)應(yīng)的 token遭庶,則表示重復(fù)操作宁仔,直接返回指定的結(jié)果給客戶(hù)端
注意:
- 對(duì) redis 中是否存在 token 以及刪除的代碼邏輯建議用 Lua 腳本實(shí)現(xiàn),保證原子性
- 全局唯一 ID 可以用百度的 uid-generator峦睡、美團(tuán)的 Leaf 去生成
2翎苫、基于 mysql 實(shí)現(xiàn)
這種實(shí)現(xiàn)方式是利用 mysql 唯一索引的特性。
示意圖如下:
具體流程步驟:
- 建立一張去重表榨了,其中某個(gè)字段需要建立唯一索引
- 客戶(hù)端去請(qǐng)求服務(wù)端煎谍,服務(wù)端會(huì)將這次請(qǐng)求的一些信息插入這張去重表中
- 因?yàn)楸碇心硞€(gè)字段帶有唯一索引,如果插入成功龙屉,證明表中沒(méi)有這次請(qǐng)求的信息粱快,則執(zhí)行后續(xù)的業(yè)務(wù)邏輯
- 如果插入失敗,則代表已經(jīng)執(zhí)行過(guò)當(dāng)前請(qǐng)求叔扼,直接返回
3事哭、基于 redis 實(shí)現(xiàn)
這種實(shí)現(xiàn)方式是基于 SETNX 命令實(shí)現(xiàn)的
SETNX key value:將 key 的值設(shè)為 value ,當(dāng)且僅當(dāng) key 不存在瓜富。若給定的 key 已經(jīng)存在鳍咱,則 SETNX 不做任何動(dòng)作。
該命令在設(shè)置成功時(shí)返回 1与柑,設(shè)置失敗時(shí)返回 0谤辜。
示意圖如下:
具體流程步驟:
- 客戶(hù)端先請(qǐng)求服務(wù)端蓄坏,會(huì)拿到一個(gè)能代表這次請(qǐng)求業(yè)務(wù)的唯一字段
- 將該字段以 SETNX 的方式存入 redis 中,并根據(jù)業(yè)務(wù)設(shè)置相應(yīng)的超時(shí)時(shí)間
- 如果設(shè)置成功丑念,證明這是第一次請(qǐng)求涡戳,則執(zhí)行后續(xù)的業(yè)務(wù)邏輯
- 如果設(shè)置失敗,則代表已經(jīng)執(zhí)行過(guò)當(dāng)前請(qǐng)求脯倚,直接返回
開(kāi)源實(shí)現(xiàn)
idempotent 冪等處理方案
1.原理
1.請(qǐng)求開(kāi)始前渔彰,根據(jù)key查詢(xún) 查到結(jié)果:報(bào)錯(cuò) 未查到結(jié)果:存入key-value-expireTime key=ip+url+args
2.請(qǐng)求結(jié)束后,直接刪除key 不管key是否存在推正,直接刪除 是否刪除恍涂,可配置
3.expireTime過(guò)期時(shí)間,防止一個(gè)請(qǐng)求卡死植榕,會(huì)一直阻塞再沧,超過(guò)過(guò)期時(shí)間,自動(dòng)刪除 過(guò)期時(shí)間要大于業(yè)務(wù)執(zhí)行時(shí)間尊残,需要大概評(píng)估下;
4.此方案直接切的是接口請(qǐng)求層面炒瘸。
5.過(guò)期時(shí)間需要大于業(yè)務(wù)執(zhí)行時(shí)間,否則業(yè)務(wù)請(qǐng)求1進(jìn)來(lái)還在執(zhí)行中寝衫,前端未做遮罩顷扩,或者用戶(hù)跳轉(zhuǎn)頁(yè)面后再回來(lái)做重復(fù)請(qǐng)求2,在業(yè)務(wù)層面上看竞端,結(jié)果依舊是不符合預(yù)期的。
6.建議delKey = false庙睡。即使業(yè)務(wù)執(zhí)行完事富,也不刪除key,強(qiáng)制鎖expireTime的時(shí)間乘陪。預(yù)防5的情況發(fā)生统台。
7.實(shí)現(xiàn)思路:同一個(gè)請(qǐng)求ip和接口,相同參數(shù)的請(qǐng)求啡邑,在expireTime內(nèi)多次請(qǐng)求贱勃,只允許成功一次。
8.頁(yè)面做遮罩谤逼,數(shù)據(jù)庫(kù)層面的唯一索引贵扰,先查詢(xún)?cè)偬砑樱忍幚矸绞綉?yīng)該都處理下流部。
9.此注解只用于冪等戚绕,不用于鎖,100個(gè)并發(fā)這種壓測(cè)枝冀,會(huì)出現(xiàn)問(wèn)題舞丛,在這種場(chǎng)景下也沒(méi)有意義耘子,實(shí)際中用戶(hù)也不會(huì)出現(xiàn)1s或者3s內(nèi)手動(dòng)發(fā)送了50個(gè)或者100個(gè)重復(fù)請(qǐng)求,或者弱網(wǎng)下有100個(gè)重復(fù)請(qǐng)求;
2.使用
- 引入依賴(lài)
<dependency>
<groupId>com.pig4cloud.plugin</groupId>
<artifactId>idempotent-spring-boot-starter</artifactId>
<version>0.0.3</version>
</dependency>
- 配置 redis 鏈接相關(guān)信息
spring:
redis:
host: 127.0.0.1
port: 6379
理論是支持 redisson-spring-boot-starter 全部配置
- 接口設(shè)置注解
@Idempotent(key = "#demo.username", expireTime = 3, info = "請(qǐng)勿重復(fù)查詢(xún)")
@GetMapping("/test")
public String test(Demo demo) {
return "success";
}
注解 配置詳細(xì)說(shuō)明
- 冪等操作的唯一標(biāo)識(shí)球切,使用spring el表達(dá)式 用#來(lái)引用方法參數(shù) 谷誓。 可為空則取 當(dāng)前 url + args 做表示
String key();
- 有效期 默認(rèn):1 有效期要大于程序執(zhí)行時(shí)間,否則請(qǐng)求還是可能會(huì)進(jìn)來(lái)
int expireTime() default 1;
- 時(shí)間單位 默認(rèn):s (秒)
TimeUnit timeUnit() default TimeUnit.SECONDS;
- 冪等失敗提示信息吨凑,可自定義
String info() default "重復(fù)請(qǐng)求捍歪,請(qǐng)稍后重試";
- 是否在業(yè)務(wù)完成后刪除key true:刪除 false:不刪除
boolean delKey() default false;
總結(jié)
這幾種實(shí)現(xiàn)冪等的方式其實(shí)都是大同小異的,類(lèi)似的還有使用狀態(tài)機(jī)怀骤、悲觀鎖费封、樂(lè)觀鎖的方式來(lái)實(shí)現(xiàn),都是比較簡(jiǎn)單的蒋伦。