文章目的:本文旨在提煉一套分佈式冪等問題的思考框架,而非解決某個具體的分佈式冪等問題。在這個框架體系內,會有一些方案舉例說明。
文章目標:但願讀者能經過這套思考框架設計出符合本身業務的完備的冪等解決方案。
文章內容:
(1)背景介紹,爲何會有冪等。
(2)什麼是冪等,這個定義很是重要,決定了整個思考框架。
(3)解決冪等問題的三部曲,也是做者的思考框架。
(4)總結前端
分佈式系統由衆多微服務組成,微服務之間必然存在大量的網絡調用。下圖是一個服務間調用異常的例子,用戶提交訂單以後,請求到A服務,A服務落單以後,開始調用B服務,可是在A調用B的過程當中,存在不少不肯定性,例如B服務執行超時了,RPC直接返回A請求超時了,而後A返回給用戶一些錯誤提示,但實際狀況是B有可能執行是成功的,只是執行時間過長而已。java
用戶看到錯誤提示以後,每每會選擇在界面上重複點擊,致使重複調用,若是B是個支付服務的話,用戶重複點擊可能致使同一個訂單被扣屢次錢。不只僅是用戶可能觸發重複調用,定時任務、消息投遞和機器從新啓動均可能會出現重複執行的狀況。在分佈式系統裏,服務調用出現各類異常的狀況是很常見的,這些異常狀況每每會使得系統間的狀態不一致,因此須要容錯補償設計,最多見的方法就是調用方實現合理的重試策略,被調用方實現應對重試的冪等策略。redis
對於冪等,有一個很常見的描述是:對於相同的請求應該返回相同的結果,因此查詢類接口是自然的冪等性接口。舉個例子:若是有一個查詢接口是查詢訂單的狀態,狀態是會隨着時間發生變化的,那麼在兩次不一樣時間的查詢請求中,可能返回不同的訂單狀態,這個查詢接口仍是冪等接口嗎?算法
冪等的定義直接決定了咱們如何去設計冪等方案,若是冪等的含義是相同請求返回相同結果,那實際上只須要緩存第一次的返回結果,便可在後續重複請求時實現冪等了。但問題真的有這麼簡單嗎?sql
筆者更贊同這種定義:冪等指的是相同請求(identical request)執行一次或者屢次所帶來的反作用(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).
這個定義有必定的抽象,歸納性比較強,在設計冪等方案時,其實就是將抽象部分具化。例如:什麼是相同的請求?哪些狀況會有反作用?該如何避免反作用?且看三部曲。後端
很多關於冪等的文章都稱本身的方案是通用解決方案,但筆者卻認爲,不一樣的業務場景下,相同請求和反作用都是有差別性的,不一樣的反作用須要不一樣的方案來解決,不存在徹底通用的解決方案。而三部曲旨在提煉出一種思考模式,並舉例說明,在該思考模式下,更容易設計出符合業務場景的冪等解決方案。緩存
冪等是爲了解決重複執行同一請求的問題,那如何識別一個請求有沒有和以前的請求重複呢?有的方案是經過請求中的某個流水號字段來識別的,同一個流水號表示同一個請求。也有的方案是經過請求中某幾個字段甚至所有字段進行比較,從而來識別是否爲同一個請求。因此在方案設計時,明肯定義具體業務場景下什麼是相同請求,這是第一部曲。安全
在一條調用鏈路的後端系統中,通常均可以經過上游系統傳遞的reqNo+source來識別是不是爲重複的請求。以下圖,B系統是依賴於A系統傳遞的reqNo+source來識別相同請求的,可是A系統是直接和前端頁面交互的系統,如何識別用戶發起的請求是相同的呢?好比用戶在支付界面上點擊了屢次,A系統怎麼識別這是一次重複操做呢?網絡
前端能夠在第一次點擊完成時,將按鈕設置爲disable,這樣用戶沒法在界面上重複點擊第二次,但這只是提高體驗的前端解決方案,不是真正安全的解決方案。
常見的服務端解決方案是採用token機制來實現防重複提交。以下圖,
(1)當用戶進入到表單頁面的時候,前端會從服務端申請到一個token,並保存在前端。
(2)當用戶第一次點擊提交的時候,會將該token和表單數據一併提交到服務端,服務端判斷該token是否存在,若是存在則執行業務邏輯。
(3)當用戶第二次點擊提交的時候,會將該token和表單數據一併提交到服務端,服務端判斷該token是否存在,若是不存在則返回錯誤,前端顯示提交失敗。
這個方案結合先後端,從前端視角,這是用於防止重複請求,從服務端視角,這個用於識別前端相同請求。服務端每每基於相似於redis之類的分佈式緩存來實現,保證生成token的惟一性和操做token時的原子性便可。核心邏輯以下。
// SETNX keyName value: 若是key存在,則返回0,若是不存在,則返回1 // step1. 申請token String token = generateUniqueToken(); // step2. 校驗token是否存在 if(redis.setNx(token, 1) == 1){ // do business } else { // 冪等邏輯 }
相同的請求重複執行業務邏輯,若是處理不當,會給系統帶來反作用。那什麼是反作用?從技術的角度理解就是返回結果後還致使某些「系統狀態」發生變化,無反作用的函數稱之爲純函數,體現到業務的角度就是業務沒法接受的非預期結果。最多見的有重複入庫、數據被錯誤變動等,大多數冪等方案就是圍繞解決這類問題來設計的。而系統每每可能在多個維度都存在反作用,例如:
(1)調用下游維度:重複調用下游會怎樣?若是下游沒有冪等,重複調用會帶來什麼反作用?
(2)返回上游維度:例如第一次返回上游異常,第二次返回上游被冪等了?會給上游帶來什麼反作用?
(3)併發執行維度:併發重複執行會怎樣?會有什麼反作用?
(4)分佈式鎖維度:引入分佈式鎖來防止併發執行?可是若是鎖出現不一致性,會有什麼反作用?
(5)交互時序維度:有沒有異步交互,是否存在時序問題?會有什麼反作用?
(6)客戶體驗維度:從數據不一致到最終一致,必須在多少時間內完成?若是該時間內沒有完成,會有什麼反作用?例如大量客訴(秉承客戶第一的原則,在支付寶,客訴量太大會定級爲生產環境故障)。
(7)業務覈對維度:重複調用是否存在覆蓋覈對標識的狀況,帶來沒法正常覈對的反作用?在金融系統中,資金鍊路沒法覈對是沒法接受的。
(8)數據質量維度:是否存在重複記錄?若是存在會有什麼反作用?
上面是一些常見的分析維度,不一樣行業的系統中會存在不同的維度,儘量地總結出這些維度,並列入系統分析時的checklist中,可以更好地完善冪等解決方案。沒有反作用纔算是完備的冪等解決方案,可是反作用的維度太多,會提升冪等方案的複雜度。因此在可以達成業務的前提下,減小一些分析維度,可以使得冪等方案實現起來更加經濟有效。例如:若是有專門的冪等表存儲返回給上游的冪等結果,第(2)維度不用考慮了,若是用鎖來防止併發,第(3)個維度不考慮了,若是用單機鎖代替分佈式鎖,第(4)個維度不考慮了。
這是解決冪等問題的第二部曲:列出並減小反作用的分析維度。在這部曲中,涉及的解決方案每每是解決某一個維度的反作用問題,適合以通用組件的形式存在,做爲團隊內部的一個公共技術套路。
不少冪等解決方案都和防併發有關,那麼冪等和併發到底有什麼關聯呢?二者的聯繫是:冪等解決的是重複執行的問題,重複執行既有串行重複執行(例如定時任務),也有併發重複執行。若是重複執行的業務邏輯沒有共享變量和數據變動操做時,併發重複執行是沒有反作用的,能夠不考慮併發的問題。對於包含共享變量、涉及變動操做的服務(實際上這類服務居多),併發問題可能致使亂序讀寫共享變量,重複插入數據等問題。特別是併發讀寫共享變量,每每都是發生生產故障後才被感知到。
因此在併發執行的維度,將併發重複執行變成串行重複執行是最好的冪等解決方案。支付寶最多見的方法就是:一鎖二判三更新,以下圖。當一個請求過來以後:一鎖,鎖住要操做的資源;二判,識別是否爲重複請求(第一部曲要定義的問題)、判斷業務狀態是否正常;三更新:執行業務邏輯。
Q&A
小A:鎖可能形成性能影響,先判後鎖再執行,能夠提高效能。
大明:這樣可能會失去防併發的效果。還記得double check實現單例模式嗎?在加鎖前判斷了下,那加鎖後爲啥還要判斷下?實際上第二次check纔是必須的。想一想看?
小A畫圖思考中...
小A:明白了,一鎖二判三更新,鎖和判的順序是不能變的,若是鎖衝突比較高,能夠在鎖以前判斷下,提升效率,因此稱之爲double check。
大明:是的,聰明。這兩個場景不同,但併發思路是同樣的。
private volatile static Girl theOnlyGirl; // 實現單例時作了 double check public static Girl getTheOnlyGirl() { if (theOnlyGirl == null) { // 加鎖前check synchronized (Girl.class) { if (theOnlyGirl == null) { // 加鎖後check theOnlyGirl = new Girl(); // 變動執行 } } } return theOnlyGirl; }
鎖的實現能夠是分佈式鎖,也是能夠是數據庫鎖。分佈式鎖自己會帶來鎖的一致性問題,須要根據業務對系統穩定性的要求來考量。支付寶的不少系統是經過在業務數據庫中新建一個鎖記錄表來實現業務鎖組件,其分表邏輯和業務表的分表邏輯一致,就能夠實現單機數據庫鎖。若是沒有鎖組件,悲觀鎖鎖住業務單據也是能夠知足條件的,悲觀鎖要在事務中用select for update來實現,要注意死鎖問題,且where條件中必須命中索引,不然會鎖表,不鎖記錄。
併發維度幾乎是一個分佈式冪等的通用分析維度,因此一個通用的鎖組件是頗有必要的。但這也只是解決了併發這一個維度的反作用。雖然沒有了併發重複執行的狀況,但串行重複執行的狀況依舊存在,重複執行纔是冪等核心要解決的問題,重複執行若是還存在其它反作用,冪等問題就是沒有解決掉。
加鎖後業務的性能會下降,這個怎麼解決?筆者認爲,大多數狀況下架構的穩定性比系統性能的優先級更高,何況對於性能的優化有太多地方能夠去實現,減小壞代碼、去除慢SQL、優化業務架構、水平擴展數據庫資源等方式。經過系統壓測來實現一個知足SLA的服務纔是評估全鏈路性能的正確方法。
在解決了部分維度的反作用以後,就須要針對剩餘維度存在的細粒度反作用進行逐一識別並解決了。在數據質量維度上,最大的一個反作用是重複數據。在交互維度上,最大的一個反作用是業務亂序執行。通常這類問題不設計成通用組件,能夠開發人員自由發揮。本節用兩個常見方案作爲例子。
在數據表設計時,設計兩個字段:source、reqNo,source表示調用方,seqNo表示調用方發送過來的請求號。source和reqNo設置爲組合惟一索引,保證單據不會重複落兩次。若是調用方沒有source和reqNo這兩個字段,能夠根據業務實際狀況將請求中的某幾個業務參數生成一個md5做爲惟一性字段落到惟一性字段中來避免重複落庫。
核心邏輯以下:
try { dao.insert(entity); // do business } catch (DuplicateKeyException e) { dao.select(param); // 冪等返回 }
這裏直接insert單據,若果成功則表示沒請求過,舉行執行業務邏輯,若是拋出DuplicateKeyException異常,則表示已經執行過,作冪等返回,簡單的服務經過這種方式也能夠識別是否爲重複請求(第一部曲)。
利用數據庫惟一索引來避免重複記錄,須要注意如下幾個問題:
(1)由於存在讀寫分離的設計,有可能insert操做的是主庫,但select查詢的倒是從庫,若是主備同步不及時,有可能select查出來也是空的。
(2)在數據庫有Failover機制的狀況下,若是一個城市出現天然災害,極可能切換到另一個城市的備用庫,那麼惟一性約束可能就會出現失效的狀況,好比並發場景下第一次insert是在杭州的庫,而後此時failover將庫切到上海了,再一次一樣的請求insert也是成功的。
(3)數據庫擴容場景下,由於分庫規則發生變化,有可能第一次insert操做是在A庫,第二次insert操做是在B庫,惟一索引一樣不起做用。
(4)有的系統catch的是SQLIntegrityConstraintViolationException,這個是完整性約束,包含了惟一性約束,若是未給一個必填字段設值,也會拋這個異常,因此應該catch鍵重複異常DuplicateKeyException。
對於第(1)個問題,將insert 和select放在同一個事務中便可解決,對於(2)和(3),支付寶內部爲了應對容量暴漲和FO,設計了一套基於數據複製技術的分佈式數據平臺,這個case筆者瞭解不深,後續有機會再討論。
小A:若是我用惟一性約束來保證不會落重複數據,是否是能夠不加鎖防併發了?
大明:二者沒有直接關係,加鎖防併發解決的是併發維度的反作用問題,惟一性約束只是解決重複數據這單個反作用的問題。若是沒有惟一性約束,串行重複執行也會致使insert重複落數據的問題,惟一性約束本質上解決的是重複數據問題,不是併發問題。
一個業務的生命週期每每存在不一樣的狀態,用狀態機來控制業務流程中的狀態轉換是不二之選。在實際業務中單向的狀態機是比較經常使用的,當狀態機處於下一個狀態時,是不能回到前面的狀態的。如下場景常常會用到狀態機作校驗:
(1)調用方調用超時重試。
(2)消息投遞超時重試。
(3)業務系統發起多個任務,可是期待按照發起順序有序返回。
對於這種類問題,通常是在處理前先判斷狀態是否符合預期,若是符合預期再執行業務。當業務執行完成後,變動狀態時還會採起相似於於樂觀鎖的方式兜底校驗,例如,M狀態只能從N狀態轉換而來,那麼更新單據時,會在sql中作狀態校驗。
update apply set status = 'M' where status = 'N'
若是狀態被設計成可逆的,就有可能產生ABA問題。即在update以前,狀態有可能作過這樣的變動:N -> M -> N。因此狀態機設成單向流轉是比較合理的。
本文首先引出了冪等的定義:相同請求無反作用,而後提出了設計冪等方案的三部曲,並舉例說明。設計者要可以清晰地定義相同請求,而且採用通用組件減小一些反作用的分析維度,再針對具體的反作用設計相應的解決方案,直至沒有任何反作用,纔是真正完備的冪等解決方案。在實際業務中,實現三部曲不必定是嚴格的前後順序,但只要按照這三部曲來構思方案,必能開拓思路,化繁爲簡。
公衆號簡介:做者是螞蟻金服的一線開發,分享本身的成長和思考之路。內容涉及數據、工程、算法。
注:轉載請註明出處。本文提到的分佈式鎖、業務鎖,悲觀鎖和樂觀鎖的選型,以及基於鎖的冪等組件的實現,將另起文章介紹,若感興趣能夠關注公衆號,歡迎交流。