您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關redis中如何解決分布式冪等問題的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
分布式系統由眾多微服務組成,微服務之間必然存在大量的網絡調用。下圖是一個服務間調用異常的例子,用戶提交訂單之后,請求到A服務,A服務落單之后,開始調用B服務,但是在A調用B的過程中,存在很多不確定性,例如B服務執行超時了,RPC直接返回A請求超時了,然后A返回給用戶一些錯誤提示,但實際情況是B有可能執行是成功的,只是執行時間過長而已。
用戶看到錯誤提示之后,往往會選擇在界面上重復點擊,導致重復調用,如果B是個支付服務的話,用戶重復點擊可能導致同一個訂單被扣多次錢。不僅僅是用戶可能觸發重復調用,定時任務、消息投遞和機器重新啟動都可能會出現重復執行的情況。在分布式系統里,服務調用出現各種異常的情況是很常見的,這些異常情況往往會使得系統間的狀態不一致,所以需要容錯補償設計,最常見的方法就是調用方實現合理的重試策略,被調用方實現應對重試的冪等策略。
對于冪等,有一個很常見的描述是:對于相同的請求應該返回相同的結果,所以查詢類接口是天然的冪等性接口。舉個例子:如果有一個查詢接口是查詢訂單的狀態,狀態是會隨著時間發生變化的,那么在兩次不同時間的查詢請求中,可能返回不一樣的訂單狀態,這個查詢接口還是冪等接口嗎?
冪等的定義直接決定了我們如何去設計冪等方案,如果冪等的含義是相同請求返回相同結果,那實際上只需要緩存第一次的返回結果,即可在后續重復請求時實現冪等了。但問題真的有這么簡單嗎?
筆者更贊同這種定義:冪等指的是相同請求(identical request)執行一次或者多次所帶來的副作用(side-effects)是一樣的。
引自:https://developer.mozilla.org/en-US/docs/Glossary/Idempotent 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)個維度不考慮了。
這是解決冪等問題的第二部曲:列出并減少副作用的分析維度。在這部曲中,涉及的解決方案往往是解決某一個維度的副作用問題,適合以通用組件的形式存在,作為團隊內部的一個公共技術套路。
很多冪等解決方案都和防并發有關,那么冪等和并發到底有什么關聯呢?兩者的聯系是:冪等解決的是重復執行的問題,重復執行既有串行重復執行(例如定時任務),也有并發重復執行。如果重復執行的業務邏輯沒有共享變量和數據變更操作時,并發重復執行是沒有副作用的,可以不考慮并發的問題。對于包含共享變量、涉及變更操作的服務(實際上這類服務居多),并發問題可能導致亂序讀寫共享變量,重復插入數據等問題。特別是并發讀寫共享變量,往往都是發生生產故障后才被感知到。
所以在并發執行的維度,將并發重復執行變成串行重復執行是最好的冪等解決方案。支付寶最常見的方法就是:一鎖二判三更新,如下圖。當一個請求過來之后:一鎖,鎖住要操作的資源;二判,識別是否為重復請求(第一部曲要定義的問題)、判斷業務狀態是否正常;三更新:執行業務邏輯。
小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。所以狀態機設成單向流轉是比較合理的。
本文首先引出了冪等的定義:相同請求無副作用,然后提出了設計冪等方案的三部曲,并舉例說明。設計者要能夠清晰地定義相同請求,并且采用通用組件減少一些副作用的分析維度,再針對具體的副作用設計相應的解決方案,直至沒有任何副作用,才是真正完備的冪等解決方案。在實際業務中,實現三部曲不一定是嚴格的先后順序,但只要按照這三部曲來構思方案,必能開拓思路,化繁為簡。
感謝各位的閱讀!關于“redis中如何解決分布式冪等問題”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。