摘要:解決冪等問題的三部曲,也是作者的思考框架。這是解決冪等問題的第二部曲列出并減少副作用的分析維度。所以在并發(fā)執(zhí)行的維度,將并發(fā)重復(fù)執(zhí)行變成串行重復(fù)執(zhí)行是最好的冪等解決方案。
綱要
文章目的:本文旨在提煉一套分布式冪等問題的思考框架,而非解決某個具體的分布式冪等問題。在這個框架體系內(nèi),會有一些方案舉例說明。
文章目標(biāo):希望讀者能通過這套思考框架設(shè)計出符合自己業(yè)務(wù)的完備的冪等解決方案。
文章內(nèi)容:
(1)背景介紹,為什么會有冪等。
(2)什么是冪等,這個定義非常重要,決定了整個思考框架。
(3)解決冪等問題的三部曲,也是作者的思考框架。
(4)總結(jié)
分布式系統(tǒng)由眾多微服務(wù)組成,微服務(wù)之間必然存在大量的網(wǎng)絡(luò)調(diào)用。下圖是一個服務(wù)間調(diào)用異常的例子,用戶提交訂單之后,請求到A服務(wù),A服務(wù)落單之后,開始調(diào)用B服務(wù),但是在A調(diào)用B的過程中,存在很多不確定性,例如B服務(wù)執(zhí)行超時了,RPC直接返回A請求超時了,然后A返回給用戶一些錯誤提示,但實際情況是B有可能執(zhí)行是成功的,只是執(zhí)行時間過長而已。
用戶看到錯誤提示之后,往往會選擇在界面上重復(fù)點擊,導(dǎo)致重復(fù)調(diào)用,如果B是個支付服務(wù)的話,用戶重復(fù)點擊可能導(dǎo)致同一個訂單被扣多次錢。不僅僅是用戶可能觸發(fā)重復(fù)調(diào)用,定時任務(wù)、消息投遞和機器重新啟動都可能會出現(xiàn)重復(fù)執(zhí)行的情況。在分布式系統(tǒng)里,服務(wù)調(diào)用出現(xiàn)各種異常的情況是很常見的,這些異常情況往往會使得系統(tǒng)間的狀態(tài)不一致,所以需要容錯補償設(shè)計,最常見的方法就是調(diào)用方實現(xiàn)合理的重試策略,被調(diào)用方實現(xiàn)應(yīng)對重試的冪等策略。
二 什么是冪等對于冪等,有一個很常見的描述是:對于相同的請求應(yīng)該返回相同的結(jié)果,所以查詢類接口是天然的冪等性接口。舉個例子:如果有一個查詢接口是查詢訂單的狀態(tài),狀態(tài)是會隨著時間發(fā)生變化的,那么在兩次不同時間的查詢請求中,可能返回不一樣的訂單狀態(tài),這個查詢接口還是冪等接口嗎?
冪等的定義直接決定了我們?nèi)绾稳ピO(shè)計冪等方案,如果冪等的含義是相同請求返回相同結(jié)果,那實際上只需要緩存第一次的返回結(jié)果,即可在后續(xù)重復(fù)請求時實現(xiàn)冪等了。但問題真的有這么簡單嗎?
筆者更贊同這種定義:冪等指的是相同請求(identical request)執(zhí)行一次或者多次所帶來的副作用(side-effects)是一樣的。
引自:https://developer.mozilla.org...
An HTTP method is idempotent if an identical request can be made once or several times in a row with the same effect while leaving the server in the same state. In other words, an idempotent method should not have any side-effects (except for keeping statistics).
這個定義有一定的抽象,概括性比較強,在設(shè)計冪等方案時,其實就是將抽象部分具化。例如:什么是相同的請求?哪些情況會有副作用?該如何避免副作用?且看三部曲。
三 解決方案三部曲不少關(guān)于冪等的文章都稱自己的方案是通用解決方案,但筆者卻認為,不同的業(yè)務(wù)場景下,相同請求和副作用都是有差異性的,不同的副作用需要不同的方案來解決,不存在完全通用的解決方案。而三部曲旨在提煉出一種思考模式,并舉例說明,在該思考模式下,更容易設(shè)計出符合業(yè)務(wù)場景的冪等解決方案。
第一部曲:識別相同請求冪等是為了解決重復(fù)執(zhí)行同一請求的問題,那如何識別一個請求有沒有和之前的請求重復(fù)呢?有的方案是通過請求中的某個流水號字段來識別的,同一個流水號表示同一個請求。也有的方案是通過請求中某幾個字段甚至全部字段進行比較,從而來識別是否為同一個請求。所以在方案設(shè)計時,明確定義具體業(yè)務(wù)場景下什么是相同請求,這是第一部曲。
方案舉例:token機制識別前端重復(fù)請求在一條調(diào)用鏈路的后端系統(tǒng)中,一般都可以通過上游系統(tǒng)傳遞的reqNo+source來識別是否是為重復(fù)的請求。如下圖,B系統(tǒng)是依賴于A系統(tǒng)傳遞的reqNo+source來識別相同請求的,但是A系統(tǒng)是直接和前端頁面交互的系統(tǒng),如何識別用戶發(fā)起的請求是相同的呢?比如用戶在支付界面上點擊了多次,A系統(tǒng)怎么識別這是一次重復(fù)操作呢?
前端可以在第一次點擊完成時,將按鈕設(shè)置為disable,這樣用戶無法在界面上重復(fù)點擊第二次,但這只是提升體驗的前端解決方案,不是真正安全的解決方案。
常見的服務(wù)端解決方案是采用token機制來實現(xiàn)防重復(fù)提交。如下圖,
(1)當(dāng)用戶進入到表單頁面的時候,前端會從服務(wù)端申請到一個token,并保存在前端。
(2)當(dāng)用戶第一次點擊提交的時候,會將該token和表單數(shù)據(jù)一并提交到服務(wù)端,服務(wù)端判斷該token是否存在,如果存在則執(zhí)行業(yè)務(wù)邏輯。
(3)當(dāng)用戶第二次點擊提交的時候,會將該token和表單數(shù)據(jù)一并提交到服務(wù)端,服務(wù)端判斷該token是否存在,如果不存在則返回錯誤,前端顯示提交失敗。
這個方案結(jié)合前后端,從前端視角,這是用于防止重復(fù)請求,從服務(wù)端視角,這個用于識別前端相同請求。服務(wù)端往往基于類似于redis之類的分布式緩存來實現(xiàn),保證生成token的唯一性和操作token時的原子性即可。核心邏輯如下。
// SETNX keyName value: 如果key存在,則返回0,如果不存在,則返回1 // step1. 申請token String token = generateUniqueToken(); // step2. 校驗token是否存在 if(redis.setNx(token, 1) == 1){ // do business } else { // 冪等邏輯 }第二部曲:列出并減少副作用的分析維度
相同的請求重復(fù)執(zhí)行業(yè)務(wù)邏輯,如果處理不當(dāng),會給系統(tǒng)帶來副作用。那什么是副作用?從技術(shù)的角度理解就是返回結(jié)果后還導(dǎo)致某些“系統(tǒng)狀態(tài)”發(fā)生變化,無副作用的函數(shù)稱之為純函數(shù),體現(xiàn)到業(yè)務(wù)的角度就是業(yè)務(wù)無法接受的非預(yù)期結(jié)果。最常見的有重復(fù)入庫、數(shù)據(jù)被錯誤變更等,大多數(shù)冪等方案就是圍繞解決這類問題來設(shè)計的。而系統(tǒng)往往可能在多個維度都存在副作用,例如:
(1)調(diào)用下游維度:重復(fù)調(diào)用下游會怎樣?如果下游沒有冪等,重復(fù)調(diào)用會帶來什么副作用?
(2)返回上游維度:例如第一次返回上游異常,第二次返回上游被冪等了?會給上游帶來什么副作用?
(3)并發(fā)執(zhí)行維度:并發(fā)重復(fù)執(zhí)行會怎樣?會有什么副作用?
(4)分布式鎖維度:引入分布式鎖來防止并發(fā)執(zhí)行?但是如果鎖出現(xiàn)不一致性,會有什么副作用?
(5)交互時序維度:有沒有異步交互,是否存在時序問題?會有什么副作用?
(6)客戶體驗維度:從數(shù)據(jù)不一致到最終一致,必須在多少時間內(nèi)完成?如果該時間內(nèi)沒有完成,會有什么副作用?例如大量客訴(秉承客戶第一的原則,在支付寶,客訴量太大會定級為生產(chǎn)環(huán)境故障)。
(7)業(yè)務(wù)核對維度:重復(fù)調(diào)用是否存在覆蓋核對標(biāo)識的情況,帶來無法正常核對的副作用?在金融系統(tǒng)中,資金鏈路無法核對是無法接受的。
(8)數(shù)據(jù)質(zhì)量維度:是否存在重復(fù)記錄?如果存在會有什么副作用?
上面是一些常見的分析維度,不同行業(yè)的系統(tǒng)中會存在不一樣的維度,盡可能地總結(jié)出這些維度,并列入系統(tǒng)分析時的checklist中,能夠更好地完善冪等解決方案。沒有副作用才算是完備的冪等解決方案,但是副作用的維度太多,會提高冪等方案的復(fù)雜度。所以在能夠達成業(yè)務(wù)的前提下,減少一些分析維度,能夠使得冪等方案實現(xiàn)起來更加經(jīng)濟有效。例如:如果有專門的冪等表存儲返回給上游的冪等結(jié)果,第(2)維度不用考慮了,如果用鎖來防止并發(fā),第(3)個維度不考慮了,如果用單機鎖代替分布式鎖,第(4)個維度不考慮了。
這是解決冪等問題的第二部曲:列出并減少副作用的分析維度。在這部曲中,涉及的解決方案往往是解決某一個維度的副作用問題,適合以通用組件的形式存在,作為團隊內(nèi)部的一個公共技術(shù)套路。
方案舉例:加鎖避免并發(fā)重復(fù)執(zhí)行很多冪等解決方案都和防并發(fā)有關(guān),那么冪等和并發(fā)到底有什么關(guān)聯(lián)呢?兩者的聯(lián)系是:冪等解決的是重復(fù)執(zhí)行的問題,重復(fù)執(zhí)行既有串行重復(fù)執(zhí)行(例如定時任務(wù)),也有并發(fā)重復(fù)執(zhí)行。如果重復(fù)執(zhí)行的業(yè)務(wù)邏輯沒有共享變量和數(shù)據(jù)變更操作時,并發(fā)重復(fù)執(zhí)行是沒有副作用的,可以不考慮并發(fā)的問題。對于包含共享變量、涉及變更操作的服務(wù)(實際上這類服務(wù)居多),并發(fā)問題可能導(dǎo)致亂序讀寫共享變量,重復(fù)插入數(shù)據(jù)等問題。特別是并發(fā)讀寫共享變量,往往都是發(fā)生生產(chǎn)故障后才被感知到。
所以在并發(fā)執(zhí)行的維度,將并發(fā)重復(fù)執(zhí)行變成串行重復(fù)執(zhí)行是最好的冪等解決方案。支付寶最常見的方法就是:一鎖二判三更新,如下圖。當(dāng)一個請求過來之后:一鎖,鎖住要操作的資源;二判,識別是否為重復(fù)請求(第一部曲要定義的問題)、判斷業(yè)務(wù)狀態(tài)是否正常;三更新:執(zhí)行業(yè)務(wù)邏輯。
Q&A
小A:鎖可能造成性能影響,先判后鎖再執(zhí)行,可以提升效能。
大明:這樣可能會失去防并發(fā)的效果。還記得double check實現(xiàn)單例模式嗎?在加鎖前判斷了下,那加鎖后為啥還要判斷下?實際上第二次check才是必須的。想想看?
小A畫圖思考中...
小A:明白了,一鎖二判三更新,鎖和判的順序是不能變的,如果鎖沖突比較高,可以在鎖之前判斷下,提高效率,所以稱之為double check。
大明:是的,聰明。這兩個場景不一樣,但并發(fā)思路是一樣的。
private volatile static Girl theOnlyGirl; // 實現(xiàn)單例時做了 double check public static Girl getTheOnlyGirl() { if (theOnlyGirl == null) { // 加鎖前check synchronized (Girl.class) { if (theOnlyGirl == null) { // 加鎖后check theOnlyGirl = new Girl(); // 變更執(zhí)行 } } } return theOnlyGirl; }
鎖的實現(xiàn)可以是分布式鎖,也是可以是數(shù)據(jù)庫鎖。分布式鎖本身會帶來鎖的一致性問題,需要根據(jù)業(yè)務(wù)對系統(tǒng)穩(wěn)定性的要求來考量。支付寶的很多系統(tǒng)是通過在業(yè)務(wù)數(shù)據(jù)庫中新建一個鎖記錄表來實現(xiàn)業(yè)務(wù)鎖組件,其分表邏輯和業(yè)務(wù)表的分表邏輯一致,就可以實現(xiàn)單機數(shù)據(jù)庫鎖。如果沒有鎖組件,悲觀鎖鎖住業(yè)務(wù)單據(jù)也是可以滿足條件的,悲觀鎖要在事務(wù)中用select for update來實現(xiàn),要注意死鎖問題,且where條件中必須命中索引,否則會鎖表,不鎖記錄。
并發(fā)維度幾乎是一個分布式冪等的通用分析維度,所以一個通用的鎖組件是很有必要的。但這也只是解決了并發(fā)這一個維度的副作用。雖然沒有了并發(fā)重復(fù)執(zhí)行的情況,但串行重復(fù)執(zhí)行的情況依舊存在,重復(fù)執(zhí)行才是冪等核心要解決的問題,重復(fù)執(zhí)行如果還存在其它副作用,冪等問題就是沒有解決掉。
加鎖后業(yè)務(wù)的性能會降低,這個怎么解決?筆者認為,大多數(shù)情況下架構(gòu)的穩(wěn)定性比系統(tǒng)性能的優(yōu)先級更高,況且對于性能的優(yōu)化有太多地方可以去實現(xiàn),減少壞代碼、去除慢SQL、優(yōu)化業(yè)務(wù)架構(gòu)、水平擴展數(shù)據(jù)庫資源等方式。通過系統(tǒng)壓測來實現(xiàn)一個滿足SLA的服務(wù)才是評估全鏈路性能的正確方法。
第三部:識別細粒度副作用,針對性設(shè)計解決方案在解決了部分維度的副作用之后,就需要針對剩余維度存在的細粒度副作用進行逐一識別并解決了。在數(shù)據(jù)質(zhì)量維度上,最大的一個副作用是重復(fù)數(shù)據(jù)。在交互維度上,最大的一個副作用是業(yè)務(wù)亂序執(zhí)行。一般這類問題不設(shè)計成通用組件,可以開發(fā)人員自由發(fā)揮。本節(jié)用兩個常見方案做為例子。
方案舉例1:唯一性約束避免重復(fù)落庫在數(shù)據(jù)表設(shè)計時,設(shè)計兩個字段:source、reqNo,source表示調(diào)用方,seqNo表示調(diào)用方發(fā)送過來的請求號。source和reqNo設(shè)置為組合唯一索引,保證單據(jù)不會重復(fù)落兩次。如果調(diào)用方?jīng)]有source和reqNo這兩個字段,可以根據(jù)業(yè)務(wù)實際情況將請求中的某幾個業(yè)務(wù)參數(shù)生成一個md5作為唯一性字段落到唯一性字段中來避免重復(fù)落庫。
核心邏輯如下:
try { dao.insert(entity); // do business } catch (DuplicateKeyException e) { dao.select(param); // 冪等返回 }
這里直接insert單據(jù),若果成功則表示沒請求過,舉行執(zhí)行業(yè)務(wù)邏輯,如果拋出DuplicateKeyException異常,則表示已經(jīng)執(zhí)行過,做冪等返回,簡單的服務(wù)通過這種方式也可以識別是否為重復(fù)請求(第一部曲)。
利用數(shù)據(jù)庫唯一索引來避免重復(fù)記錄,需要注意以下幾個問題:
(1)因為存在讀寫分離的設(shè)計,有可能insert操作的是主庫,但select查詢的卻是從庫,如果主備同步不及時,有可能select查出來也是空的。
(2)在數(shù)據(jù)庫有Failover機制的情況下,如果一個城市出現(xiàn)自然災(zāi)害,很可能切換到另外一個城市的備用庫,那么唯一性約束可能就會出現(xiàn)失效的情況,比如并發(fā)場景下第一次insert是在杭州的庫,然后此時failover將庫切到上海了,再一次同樣的請求insert也是成功的。
(3)數(shù)據(jù)庫擴容場景下,因為分庫規(guī)則發(fā)生變化,有可能第一次insert操作是在A庫,第二次insert操作是在B庫,唯一索引同樣不起作用。
(4)有的系統(tǒng)catch的是SQLIntegrityConstraintViolationException,這個是完整性約束,包含了唯一性約束,如果未給一個必填字段設(shè)值,也會拋這個異常,所以應(yīng)該catch鍵重復(fù)異常DuplicateKeyException。
對于第(1)個問題,將insert 和select放在同一個事務(wù)中即可解決,對于(2)和(3),支付寶內(nèi)部為了應(yīng)對容量暴漲和FO,設(shè)計了一套基于數(shù)據(jù)復(fù)制技術(shù)的分布式數(shù)據(jù)平臺,這個case筆者了解不深,后續(xù)有機會再討論。
小A:如果我用唯一性約束來保證不會落重復(fù)數(shù)據(jù),是不是可以不加鎖防并發(fā)了?方案舉例2:狀態(tài)機約束解決亂序問題
大明:兩者沒有直接關(guān)系,加鎖防并發(fā)解決的是并發(fā)維度的副作用問題,唯一性約束只是解決重復(fù)數(shù)據(jù)這單個副作用的問題。如果沒有唯一性約束,串行重復(fù)執(zhí)行也會導(dǎo)致insert重復(fù)落數(shù)據(jù)的問題,唯一性約束本質(zhì)上解決的是重復(fù)數(shù)據(jù)問題,不是并發(fā)問題。
一個業(yè)務(wù)的生命周期往往存在不同的狀態(tài),用狀態(tài)機來控制業(yè)務(wù)流程中的狀態(tài)轉(zhuǎn)換是不二之選。在實際業(yè)務(wù)中單向的狀態(tài)機是比較常用的,當(dāng)狀態(tài)機處于下一個狀態(tài)時,是不能回到前面的狀態(tài)的。以下場景經(jīng)常會用到狀態(tài)機做校驗:
(1)調(diào)用方調(diào)用超時重試。
(2)消息投遞超時重試。
(3)業(yè)務(wù)系統(tǒng)發(fā)起多個任務(wù),但是期待按照發(fā)起順序有序返回。
對于這種類問題,一般是在處理前先判斷狀態(tài)是否符合預(yù)期,如果符合預(yù)期再執(zhí)行業(yè)務(wù)。當(dāng)業(yè)務(wù)執(zhí)行完成后,變更狀態(tài)時還會采取類似于于樂觀鎖的方式兜底校驗,例如,M狀態(tài)只能從N狀態(tài)轉(zhuǎn)換而來,那么更新單據(jù)時,會在sql中做狀態(tài)校驗。
update apply set status = "M" where status = "N"
如果狀態(tài)被設(shè)計成可逆的,就有可能產(chǎn)生ABA問題。即在update之前,狀態(tài)有可能做過這樣的變更:N -> M -> N。所以狀態(tài)機設(shè)成單向流轉(zhuǎn)是比較合理的。
四 總結(jié)本文首先引出了冪等的定義:相同請求無副作用,然后提出了設(shè)計冪等方案的三部曲,并舉例說明。設(shè)計者要能夠清晰地定義相同請求,并且采用通用組件減少一些副作用的分析維度,再針對具體的副作用設(shè)計相應(yīng)的解決方案,直至沒有任何副作用,才是真正完備的冪等解決方案。在實際業(yè)務(wù)中,實現(xiàn)三部曲不一定是嚴(yán)格的先后順序,但只要按照這三部曲來構(gòu)思方案,必能開拓思路,化繁為簡。
公眾號簡介:作者是螞蟻金服的一線開發(fā),分享自己的成長和思考之路。內(nèi)容涉及數(shù)據(jù)、工程、算法。
注:轉(zhuǎn)載請注明出處。本文提到的分布式鎖、業(yè)務(wù)鎖,悲觀鎖和樂觀鎖的選型,以及基于鎖的冪等組件的實現(xiàn),將另起文章介紹,若感興趣可以關(guān)注公眾號,歡迎交流。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/76035.html
摘要:分布式系統(tǒng)錯綜復(fù)雜,今天,我們著重對分布式系統(tǒng)的互斥性與冪等性進行分析與解決。阻塞鎖與自旋鎖。公平鎖與非公平鎖。實現(xiàn)今天重點講解使用實現(xiàn)分布式鎖。個人感覺是最適合實現(xiàn)分布式鎖。如以上流程,接口無法冪等,可能導(dǎo)致重復(fù)扣款。 背景 隨著數(shù)據(jù)量的增大,用戶的增多,系統(tǒng)的并發(fā)訪問越來越大,傳統(tǒng)的單機已經(jīng)滿足不了需求,分布式系統(tǒng)成為一種必然的趨勢。分布式系統(tǒng)錯綜復(fù)雜,今天,我們著重對分布式系統(tǒng)的...
摘要:問題是,重復(fù)請求導(dǎo)致的數(shù)據(jù)重復(fù)插入。這問題造成的后果很明顯數(shù)據(jù)冗余,可能不單單多一條有些業(yè)務(wù)需求不能有多余數(shù)據(jù),造成服務(wù)問題問題如圖所示解決方式如何將同請求,不執(zhí)行插入,而是讀取前一個請求插入的數(shù)據(jù)并返回。那么使用分布式鎖的解決方案。 摘要: 原創(chuàng)出處 https://www.bysocket.com 「公眾號:泥瓦匠BYSocket 」歡迎關(guān)注和轉(zhuǎn)載,保留摘要,謝謝!目錄 為啥要解...
摘要:然而在微服務(wù)化之前,建議先進行容器化,在容器化之前,建議先無狀態(tài)化,當(dāng)整個流程容器化了,以后的微服務(wù)拆分才會水到渠成。 此文已由作者劉超授權(quán)網(wǎng)易云社區(qū)發(fā)布。 歡迎訪問網(wǎng)易云社區(qū),了解更多網(wǎng)易技術(shù)產(chǎn)品運營經(jīng)驗。 一、為什么要做無狀態(tài)化和容器化 很多應(yīng)用拆分成微服務(wù),是為了承載高并發(fā),往往一個進程扛不住這么大的量,因而需要拆分成多組進程,每組進程承載特定的工作,根據(jù)并發(fā)的壓力用多個副本公共...
摘要:總結(jié)的時間復(fù)雜度是,是空間是使用輔助棧來存儲最小值。項目就是為了解決配置繁瑣的問題,最大化的實現(xiàn)約定大于配置。 前言 只有光頭才能變強 Redis目前還在看,今天來分享一下我在秋招看過(遇到)的一些面試題(相對比較常見的) 0、final關(guān)鍵字 簡要說一下final關(guān)鍵字,final可以用來修飾什么? 這題我是在真實的面試中遇到的,當(dāng)時答得不太好,現(xiàn)在來整理一下吧。 final可以修飾...
閱讀 2092·2021-10-12 10:12
閱讀 808·2021-09-24 09:47
閱讀 1213·2021-08-19 11:12
閱讀 3501·2019-08-29 13:06
閱讀 711·2019-08-26 11:43
閱讀 2602·2019-08-23 17:20
閱讀 1177·2019-08-23 16:52
閱讀 2624·2019-08-23 14:27