接口的幂等设计
接口冪等
什么是接口冪等?就是一個接口,被重復(fù)調(diào)用多次,卻能夠保證對系統(tǒng)內(nèi)部產(chǎn)生的影響是一致的,也就是調(diào)用多次和調(diào)用一次,數(shù)據(jù)的變化是一樣的,是相同的,不會因為調(diào)用多次而出現(xiàn)任何數(shù)據(jù)問題。分布式系統(tǒng)中,接口冪等性是系統(tǒng)可行性論證的第一個步驟。很多地方需要把接口設(shè)計成冪等。
思路基本上是3種:
1 當(dāng)?shù)贜(N>1)次請求過來時,系統(tǒng)要能知道,這個業(yè)務(wù)我們已經(jīng)處理過了,相同的請求我們忽略掉就好了
2?當(dāng)?shù)贜(N>1)次請求過來時,不管三七二十一,執(zhí)行執(zhí)行之,底層的數(shù)據(jù)接口層面保證其冪等就好了
3 從源頭上避免請求重復(fù)提交。當(dāng)然,這個有一定的限制。對于用戶重復(fù)點擊,那么容易避免,代理端可以進行各種過濾,去重。但對于mq的情況等,可能無法避免。
雖然概念上很接近,我們也很容易混為一談,但服務(wù)接口的冪等和數(shù)據(jù)接口的冪等 ,細(xì)分開來還是有所不同的。 服務(wù)接口(我們的service層)可能包括了 對數(shù)據(jù)的操作,對文件的操作,對網(wǎng)絡(luò)的操作,對cpu、內(nèi)存的計算,還有對其他服務(wù)的操作; 而數(shù)據(jù)接口(我們的dao層)常常限于對數(shù)據(jù)庫表的CRUD(這里不討論廣義的“數(shù)據(jù)”的定義,而是內(nèi)存、緩存、文件、數(shù)據(jù)庫分開討論),及其復(fù)合操作。
數(shù)據(jù)接口層面的冪等設(shè)計
所有的數(shù)據(jù)接口都可以歸結(jié)為增刪改查四大類;當(dāng)然,下面我們對這四大類接口進行分析;
查詢和刪除
查詢和刪除業(yè)務(wù),天然的具有冪等的特性;
1. 查詢
在數(shù)據(jù)不變的情況下,查詢一次和查詢多次,查詢結(jié)果是一樣的;
2. 刪除
刪除一次和多次刪除的結(jié)果都是把數(shù)據(jù)刪除;(如果第二次刪除返回0 rows affected之類的,那么忽略即可)
3.?新增
我們可以把關(guān)鍵的 業(yè)務(wù)id 設(shè)置成唯一索引,這樣,第二次會失敗,唯一索引約束錯誤的話,調(diào)用方忽略即可。否則就出現(xiàn)了多條數(shù)據(jù),一條正確的,其余的是臟數(shù)據(jù)。
主要就是通過業(yè)務(wù)的相關(guān)的字段組成的一個 唯一性的約束。執(zhí)行消息處理之前可以先根據(jù)這個唯一約束是否存在,如果存在,說明已經(jīng)執(zhí)行過了,忽略即可,否則把這個唯一約束以某種方式 保存起來。大部分情況 都會存在 至少一個 業(yè)務(wù)的唯一性的約束, 比如用戶的 郵箱不能重復(fù)吧。?
4.?更新
如果是冪等的更新操作,比如update table1 set f1 = v1,我們可以不用管。因為這些操作本身是冪等的。 否則我們可能需要對update 語句進行稍稍的改寫,增加where 條件,也就是樂觀鎖的方式。比如?update table1 set f1 = v1 where f1 = v0 (v0 是初始值)。 這樣第一次update會成功,后面的 就會失敗。我們需要盡量的把那些非冪等的update sql改寫。?但是這樣,有一定限制就是我們需要 更多的參數(shù),比如這個初始值。?而且,我們不能排除某些操作是 不能改寫的, 比如給用戶增加積分等等, 原始積分就是限制的參數(shù), 如果不能提供,那么無法改寫。
服務(wù)接口層面的冪等設(shè)計
查詢業(yè)務(wù)
我們可以首先把數(shù)據(jù)加載到緩存,然后盡量保證一個高可用的緩存,同時在 新增、更新、刪除的時候維護緩存。
新增業(yè)務(wù)
新增業(yè)務(wù)類接口,我們要解決如下兩個問題
1. 同一個用戶用同樣的數(shù)據(jù)多次請求同一個接口(不管是什么原因多次提交,他應(yīng)該只請求一次)
2. 不同用戶的提交同樣的數(shù)據(jù)請求同一個接口;
第一個問題可以通過防重復(fù)提交來解決;業(yè)務(wù)數(shù)據(jù)連同Token,一起提交給接口,同一個Token,只能被處理一次(這里要注意,只能被處理一次,應(yīng)該改成只能被正確的處理一次,也就是說,我們應(yīng)該緩存某次新增業(yè)務(wù)處理的結(jié)果,如果上一次請求時出現(xiàn)某些異常,比如數(shù)據(jù)庫連接失敗,用戶再次提交的時候,我們應(yīng)該放行用戶的這次請求,當(dāng)然有些異常就不需要放行了,比如提交的業(yè)務(wù)數(shù)據(jù)不對等);
第二個問題是無法解決的,一個開放的系統(tǒng),不能杜絕兩個不同的客戶端(用戶)同時請求;但是可以交給數(shù)據(jù)的最后防線,存儲層;通過唯一索引或唯一組合索引可以防止新增數(shù)據(jù)存在臟數(shù)據(jù) (當(dāng)表存在唯一索引,并發(fā)時新增報錯時,再查詢一次就可以了,數(shù)據(jù)應(yīng)該已經(jīng)存在了,返回結(jié)果即可) ;
注意:
Token防重復(fù)提交,只需要網(wǎng)關(guān)這層控制即可;Token的處理機制,還需要緩存調(diào)用的處理結(jié)果,以判斷是否需要放行后續(xù)的重試請求;
更新業(yè)務(wù)
系統(tǒng)中的大部分業(yè)務(wù)都可以歸屬到更新業(yè)務(wù),比如禁用用戶、電商秒殺等等,只要是有更新操作的,不管是不是還有其他的操作,都?xì)w結(jié)到更新業(yè)務(wù);
更新業(yè)務(wù)接口,不僅需要有表單防重復(fù)提交的驗證,還需要有下面這些更精細(xì)的控制,以防止高并發(fā)環(huán)境中出現(xiàn)臟讀,幻讀等引起錯誤的數(shù)據(jù)更新結(jié)果;
更新業(yè)務(wù)接口冪等性解決方案一般是通過各種層面的鎖和CAS機制;
悲觀鎖
悲觀鎖,select for update,整個執(zhí)行過程中鎖定要操作的記錄;
樂觀鎖
更新業(yè)務(wù)的接口,比如訂單付款等,需要綜合使用盡可能多的信息來逐步驗證逐步減少直至杜絕重復(fù)消息重復(fù)處理的概率;基本思路是CAS(Compare And Set);
可以參考下面的兩篇文章體會一下:
1. 《架構(gòu)師之路-庫存扣多了,到底怎么整》
2. 訂單操作,利用訂單編號和訂單的狀態(tài)機(序列號)
測試用例
通過下面的方法可以初步驗證接口冪等性的健壯性:
1. 同一個請求,多次提交到同一臺節(jié)點,多次提交到不同的節(jié)點
2. 同一個請求,同時到達同一個節(jié)點,同時到達到不同的節(jié)點
3. 有邏輯先后順序的消息、請求亂序的處理,比如創(chuàng)建訂單的請求和支付訂單的請求,不能保證第一個請求先于第二個請求到達服務(wù)器;
--------------------- 摘抄至?https://blog.csdn.net/xichenguan/article/details/78085801-------------
?
消息消費流程的異常點
消息的消費確認(rèn)流程中,任何一個環(huán)節(jié)都可能會出問題!
- 方法:對于未確認(rèn)的消息,采用按規(guī)則重新投遞的方式進行處理。
- 問題:消息的重復(fù)發(fā)送會導(dǎo)致業(yè)務(wù)處理接口出現(xiàn)重復(fù)調(diào)用的問題。
消息重復(fù)發(fā)送的原因
被動方應(yīng)用接收到消息,業(yè)務(wù)處理完成后應(yīng)用出問題,消息中間件不知道消息處理結(jié)果,會重新投遞消息。
被動方應(yīng)用接收到消息,業(yè)務(wù)處理完成后網(wǎng)絡(luò)出問題,消息中間件收不到消息處理結(jié)果,會重新投遞消息。
被動方應(yīng)用接收到消息,業(yè)務(wù)處理時間過長,消息中間件因消息超時未確認(rèn),會再次投遞消息。
被動方應(yīng)用接收到消息,業(yè)務(wù)處理完成,消息中間件問題導(dǎo)致收不到消息處理結(jié)果,消息會重新投遞。
被動方應(yīng)用接收到消息,業(yè)務(wù)處理完成,消息中間件收到了消息處理結(jié)果,但由于消息存儲故障導(dǎo)致消息沒能成功確認(rèn),消息會再次投遞。
?
怎么實現(xiàn)消費方的消息冪等
1 根據(jù)業(yè)務(wù)來實現(xiàn)冪等,前面已經(jīng)說過。
2 增加消息表來實現(xiàn)冪等,主要就是說,如果無法通過業(yè)務(wù)信息判斷是否已經(jīng)消費過,那么我們通過mq的消息的id,存放到消息表,然后消費的時候先判斷是否已經(jīng)存在。通過消息ID,或生成一個唯一ID標(biāo)記每一條消息,將消息處理成功和去重日志(也就是“消息表”)通過事物的形式寫入去重表, 如果之前沒有處理成功,那么去重日志肯定是沒有記錄的,那么就消費,否則就不消費。 如果mq 會刪除 確定消費完了的數(shù)據(jù),我們可以先通過消息ID 去mq peek一下是否存在,存在則表示還未消費,然后消費方進行處理,否則就忽略。
其實這兩種方式道理都差不多,可以按照具體情況來做。
?
有時候,我們還可以 通過mq本身的消息超時機制,比如根據(jù)業(yè)務(wù)需求給消息 ID 設(shè)置一個 TTL, 或者是直接用 Redis 等緩存機制來保證在合理的時間范圍內(nèi)不會重復(fù)消費。
實現(xiàn)這些冪等是有成本的,我可以考慮業(yè)務(wù)情況,如果都每一個消費方的操作都做冪等設(shè)計, 有時候可能成本太高,得不償失。我們需要權(quán)衡去重所花的代價決定是否需要實現(xiàn)冪等性,如:購物會員卡成功,向用戶發(fā)送通知短信,發(fā)送一次或者多次影響不大。不做冪等性可以省掉寫去重日志的操作。
?
?
參考:
https://blog.csdn.net/xichenguan/article/details/78085801?
https://blog.csdn.net/qq_27384769/article/details/79307340
?
posted on 2018-12-16 16:38 CanntBelieve 閱讀(...) 評論(...) 編輯 收藏轉(zhuǎn)載于:https://www.cnblogs.com/FlyAway2013/p/10126940.html
總結(jié)
- 上一篇: 纸牌游戏CardBattle的设计与开发
- 下一篇: 搭建流媒体服务器(1)