? ? ? ?最近一直在思考怎么保障接口調(diào)用的冪等性阐滩,經(jīng)過參考網(wǎng)上的一些資料結合自身的情況而有所得忧饭,現(xiàn)整理如下,做個備忘鳞骤,有興趣或者有同樣需求的朋友希望可以借此找到適合你們的方法窒百。
一、什么是接口冪等性
首先看下從網(wǎng)上扒了一段對冪等性的概念描述:
冪等性原本是數(shù)學上的概念豫尽,用在接口上就可以理解為:同一個接口篙梢,多次發(fā)出同一個請求,必須保證操作只執(zhí)行一次美旧。?調(diào)用接口發(fā)生異常并且重復嘗試時渤滞,總是會造成系統(tǒng)所無法承受的損失,所以必須阻止這種現(xiàn)象的發(fā)生榴嗅。
例如下面這些情況妄呕,如果沒有實現(xiàn)接口冪等性會有很嚴重的后果:
1、 支付接口嗽测,重復支付會導致多次扣錢 绪励;
2、訂單接口,同一個訂單可能會多次創(chuàng)建优炬。
二颁井、為什么會產(chǎn)生接口冪等性問題
什么情況下,會產(chǎn)生接口冪等性的問題呢蠢护?下面列舉了各種有可能產(chǎn)生接口冪等性問題的場景雅宾。
1、網(wǎng)絡波動, 可能會引起重復請求
2葵硕、用戶在操作時候可能會無意觸發(fā)多次下單交易,甚至沒有響應而有意觸發(fā)多次交易應用
3眉抬、使用了失效或超時重試機制(Nginx重試、RPC重試或業(yè)務層重試等)
4懈凹、頁面重復刷新
5蜀变、使用瀏覽器后退按鈕重復之前的操作,導致重復提交表單
6、使用瀏覽器歷史記錄重復提交表單
8介评、定時任務重復執(zhí)行
9库北、用戶雙擊提交按鈕
三、如何保證接口冪等性们陆?
那么寒瓦,如何保證接口冪等性呢?
解決辦法一般分為兩個方向坪仇,一個方向是客戶端防止重復調(diào)用杂腰,一個是服務端進行校驗。
1椅文、客戶端防止重復調(diào)用
? ? ?a喂很、 按鈕只可操作一次
? ? ? ? ??提交后把按鈕置灰或loding狀態(tài),消除用戶因為重復點擊而產(chǎn)生的重復記錄,但是客戶端防止重復提交并不是絕對可靠的皆刺,優(yōu)點是實現(xiàn)起來比較簡單少辣。
? ? b、使用Post/Redirect/Get模式
? ? ? ? ?在提交后執(zhí)行頁面重定向,這就是所謂的Post-Redirect—Get(PRG)模式,簡單來說就是當用戶提交連表單后,跳轉到一個重定向的信息頁面,這樣就避免用戶按F5刷新導致的重復提交,而且也不會出現(xiàn)瀏覽器表單重復提交的警告,也能消除按瀏覽器前進和后退導致同樣重復提交的問題芹橡。
2毒坛、服務端防止重復調(diào)用
a、token機制
功能上允許重復提交,但要保證重復提交不產(chǎn)生副作用,比如點擊n次只產(chǎn)生一條記錄,具體實現(xiàn)就是進入頁面時申請一個token,然后后面所有的請求都帶上這個token,后端根據(jù)token來避免重復請求林说。改方案需要客戶端先請求token接口煎殷,然后再調(diào)業(yè)務接口,調(diào)用流程比較復雜腿箩,流程圖如下:
b豪直、使用唯一索引防止新增臟數(shù)據(jù)
利用數(shù)據(jù)庫唯一索引機制,當數(shù)據(jù)重復時,插入數(shù)據(jù)庫會拋出異常,保證不會出現(xiàn)臟數(shù)據(jù)。但不適用于數(shù)據(jù)庫分片的場景
c珠移、樂觀鎖
如果更新已有數(shù)據(jù),可以進行加鎖更新,也可以設計表結構時使用樂觀鎖,通過version來做樂觀鎖,這樣既能保證執(zhí)行效率,又能保證冪等, 樂觀鎖的version版本在更新業(yè)務數(shù)據(jù)要自增
update table set version = version + 1 where id = #{id} and version = #{version}
示例: 當有重復請求的時候,第一個請求會獲取當前商品的version版本號,得到的version為1,緊接著由于第一個請求還沒更新商品的version,第二個請求獲取的version依然也是1, 這時候第一個請求操作更新的時候帶上version并作為條件并且自增更新,這時候商品的version就會變成2,當?shù)诙€請求去操作更新的時候明顯version不一致導致更新失敗弓乙。
select + insert or update or delete
該方案就是操作之前先查詢一下,符合要求再插入,該方案在沒有并發(fā)的系統(tǒng)中可以解決冪等問題末融,在單JVM有并發(fā)的時候可以用JVM加鎖來保證冪等性,在分布式環(huán)境它是無法保證冪等性,可以使用分布式來保證。
d暇韧、分布式鎖
如果是分布是系統(tǒng)勾习,構建全局唯一索引比較困難,例如唯一性的字段沒法確定懈玻,這時候可以引入分布式鎖巧婶,通過第三方的系統(tǒng)(redis或zookeeper),在業(yè)務系統(tǒng)插入數(shù)據(jù)或者更新數(shù)據(jù)涂乌,獲取分布式鎖艺栈,然后做操作,之后釋放鎖湾盒,這樣其實是把多線程并發(fā)的鎖的思路湿右,引入多多個系統(tǒng),也就是分布式系統(tǒng)中得解決思路罚勾。要點:某個長流程處理過程要求不能并發(fā)執(zhí)行毅人,可以在流程執(zhí)行之前根據(jù)某個標志(用戶ID+后綴等)獲取分布式鎖,其他流程執(zhí)行時獲取鎖就會失敗荧库,也就是同一時間該流程只能有一個能執(zhí)行成功堰塌,執(zhí)行完成后赵刑,釋放分布式鎖(分布式鎖要第三方系統(tǒng)提供)分衫。
e、狀態(tài)機冪等
在設計單據(jù)相關的業(yè)務般此,或者是任務相關的業(yè)務蚪战,肯定會涉及到狀態(tài)機(狀態(tài)變更圖),就是業(yè)務單據(jù)上面有個狀態(tài)铐懊,狀態(tài)在不同的情況下會發(fā)生變更邀桑,一般情況下存在有限狀態(tài)機,這時候科乎,如果狀態(tài)機已經(jīng)處于下一個狀態(tài)壁畸,這時候來了一個上一個狀態(tài)的變更,理論上是不能夠變更的茅茂,這樣的話捏萍,保證了有限狀態(tài)機的冪等。注意:訂單等單據(jù)類業(yè)務空闲,存在很長的狀態(tài)流轉令杈,一定要深刻理解狀態(tài)機,對業(yè)務系統(tǒng)設計能力提高有很大幫助 碴倾。
f逗噩、防重表
使用唯一主鍵去做防重表的唯一索引,比如使用訂單號作為防重表的唯一索引,每一次請求都根據(jù)訂單號向防重表中插入一條數(shù)據(jù),插入成功說明可以處理后面的業(yè)務,當處理完業(yè)務邏輯之后刪除防重表中的訂單號數(shù)據(jù),后續(xù)如果有重復請求,則會因為防重表唯一索引原因導致插入失敗,直接返回操作失敗,直到第一次請求返回結果,可以看出防重表作用就是加鎖的功能掉丽。
g、緩沖隊列
將請求都快速地接收下來后放入緩沖隊列中,后續(xù)使用異步任務處理隊列中的數(shù)據(jù),過濾掉重復的請求,該解決方案優(yōu)點是同步處理改成異步處理异雁、高吞吐量,缺點則是不能及時地返回請求結果,需要后續(xù)輪詢得處理結果捶障。
總的來講接口冪等的解決方案眾多,但個人覺得實現(xiàn)成本比較大纲刀,不利于落地残邀。
四、我的設計方案
1柑蛇、首先寫個注解UnicornIdempotent
這里group屬性對接口冪等進行分組芥挣,默認為unicorn
value屬性支持spel,用來計算接口請求唯一key
throwException屬性設置是否需要將重復提交的異常拋出來反饋給客戶端
2耻台、再來一個Aspect對接口進行攔截
大體上流程如下:
a空免、計算冪等的唯一標識 key
b、嘗試給這個key加鎖盆耽,此處用redis實現(xiàn)的分布式鎖
c蹋砚、根據(jù)加鎖的情況判斷是否是重復提交
d、如果加鎖失敗是重復提交則判斷是直接拋出異常還是返回上一次調(diào)用的緩存結果
e摄杂、如果加鎖成功則為正常請求坝咐,執(zhí)行具體的業(yè)務方法之后緩存調(diào)用結果到redis中
流程圖如下:
3、使用
?a析恢、在需要冪等的接口方法上增加UnicornIdempotent注解
b墨坚、配置文件
timeWindowSecond用來配置冪等支撐的時間窗口,超過時間窗口接口不冪等映挂,可以根據(jù)自己的實際情況來設置
tips用來配置拋出異常對客戶端的提示信息
4泽篮、總結
?a、該方法易于實現(xiàn)柑船,但是存在一個時間窗口帽撑,在時間窗口內(nèi)可以保證冪等,時間窗口外不保證冪等性鞍时,這個時間窗口期需要根據(jù)自己的實際情況來設定
b亏拉、需要接口設計時能夠提煉出接口調(diào)用的唯一key,需要開發(fā)人員根據(jù)業(yè)務來定逆巍,此處如果沒有指定唯一key及塘,則默認將請求URL+請求參數(shù)+請求token進行MD5后加密獲取
c、支持兩種方式的處理:直接拋異痴粑或者返回緩存結果