事務補償


99% 的人都能看懂的「補償」以及最佳實踐

也許你對降級已經有了一些認識,這次,我們來聊一聊在保證對外高可用的同時,憋出的“內傷”該如何通過「補償」機制來自行消化。

「補償」機制的意義

以電商的購物場景為例:

客戶端 ----> 購物車微服務 ----> 訂單微服務 ----> 支付微服務。

這種調用鏈非常普遍。
 
那么為什么需要考慮補償機制呢?

正如之前幾篇文章所說,一次跨機器的通信可能會經過 DNS 服務,網卡、交換機、路由器、負載均衡等設備,這些設備都不一定是一直穩定的,在數據傳輸的整個過程中,只要任意一個環節出錯,都會導致問題的產生。

而在分布式場景中,一個完整的業務又是由多次跨機器通信組成的,所以產生問題的概率成倍數增加。

但是,這些問題並不完全代表真正的系統無法處理請求,所以我們應當盡可能的自動消化掉這些異常。

可能你會問,之前也看到過「補償」和「事務補償」或者「重試」,它們之間的關系是什么?

你其實可以不用太糾結這些名字,從目的來說都是一樣的。就是一旦某個操作發生了異常,如何通過內部機制將這個異常產生的「不一致」狀態消除掉。

題外話:在筆者看來,不管用什么方式,只要通過額外的方式解決了問題都可以理解為是「補償」,所以「事務補償」和「重試」都是「補償」的子集。前者是一個逆向操作,而后者則是一個正向操作。

只是從結果來看,兩者的意義不同。「事務補償」意味着“放棄”,當前操作必然會失敗。

99%的人都能看懂的「補償」以及最佳實踐
▲事務補償

「重試」則還有處理成功的機會。這兩種方式分別適用於不同的場景。

99%的人都能看懂的「補償」以及最佳實踐
▲重試
 
因為「補償」已經是一個額外流程了,既然能夠走這個額外流程,說明時效性並不是第一考慮的因素,所以做補償的核心要點是:寧可慢,不可錯。

因此,不要草率的就確定了補償的實施方案,需要謹慎的評估。雖說錯誤無法 100% 避免,但是抱着這樣的一個心態或多或少可以減少一些錯誤的發生。

「補償」該怎么做

做「補償」的主流方式就前面提到的「事務補償」和「重試」,以下會被稱作「回滾」和「重試」。

我們先來聊聊「回滾」。相比「重試」,它邏輯上更簡單一些。

「回滾」

回滾分為 2 種模式,一種叫「顯式回滾」(調用逆向接口),一種叫「隱式回滾」(無需調用逆向接口)。
 
最常見的就是「顯式回滾」。這個方案無非就是做 2 個事情:

首先要確定失敗的步驟和狀態,從而確定需要回滾的范圍。一個業務的流程,往往在設計之初就制定好了,所以確定回滾的范圍比較容易。但這里唯一需要注意的一點就是:如果在一個業務處理中涉及到的服務並不是都提供了「回滾接口」,那么在編排服務時應該把提供「回滾接口」的服務放在前面,這樣當后面的工作服務錯誤時還有機會「回滾」。

其次要能提供「回滾」操作使用到的業務數據。「回滾」時提供的數據越多,越有益於程序的健壯性。因為程序可以在收到「回滾」操作的時候可以做業務的檢查,比如檢查賬戶是否相等,金額是否一致等等。

由於這個中間狀態的數據結構和數據大小並不固定,所以 Z 哥建議你在實現這點的時候可以將相關的數據序列化成一個 json,然后存放到一個 nosql 類型的存儲中。

「隱式回滾」相對來說運用場景比較少。它意味着這個回滾動作你不需要進行額外處理,下游服務內部有類似“預占”並且“超時失效”的機制的。例如:

電商場景中,會將訂單中的商品先預占庫存,等待用戶在 15 分鍾內支付。如果沒有收到用戶的支付,則釋放庫存。

下面聊聊可以有很多玩法,也更容易陷入坑里的「重試」。

「重試」 

「重試」最大的好處在於,業務系統可以不需要提供「逆向接口」,這是一個對長期開發成本特別大的利好,畢竟業務是天天在變的。所以,在可能的情況下,應該優先考慮使用「重試」。

不過,相比「回滾」來說「重試」的適用場景更少一些,所以我們第一步首先要判斷,當前場景是否適合「重試」。比如:

  • 下游系統返回「請求超時」、「被限流中」等臨時狀態的時候,我們可以考慮重試
  • 而如果是返回“余額不足”、“無權限”等明確無法繼續的業務性錯誤的時候就不需要重試了
  • 一些中間件或者 rpc 框架中返回 Http503、404 等沒有何時恢復的預期的時候,也不需要重試

如果確定要進行「重試」,我們還需要選定一個合適的「重試策略」。主流的「重試策略」主要是以下幾種。

策略 1. 立即重試。有時故障是候暫時性,可能是因網絡數據包沖突或硬件組件流量高峰等事件造成的。在此情況下,適合立即重試操作。不過,立即重試次數不應超過一次,如果立即重試失敗,應改用其它的策略。

策略 2. 固定間隔。應用程序每次嘗試的間隔時間相同。 這個好理解,例如,固定每 3 秒重試操作。(以下所有示例代碼中的具體的數字僅供參考。)

策略 1 和策略 2 多用於前端系統的交互式操作中。

策略 3. 增量間隔。每一次的重試間隔時間增量遞增。比如,第一次 0 秒、第二次 3 秒、第三次 6 秒,9、12、15 這樣。

return (retryCount - 1) * incrementInterval;

使得失敗次數越多的重試請求優先級排到越后面,給新進入的重試請求讓道。

策略 4. 指數間隔。每一次的重試間隔呈指數級增加。和增量間隔“殊途同歸”,都是想讓失敗次數越多的重試請求優先級排到越后面,只不過這個方案的增長幅度更大一些。

return 2 ^ retryCount;
 
策略 5. 全抖動。在遞增的基礎上,增加隨機性(可以把其中的指數增長部分替換成增量增長。)。適用於將某一時刻集中產生的大量重試請求進行壓力分散的場景。

return random(0 , 2 ^ retryCount);
 
策略 6. 等抖動。在「指數間隔」和「全抖動」之間尋求一個中庸的方案,降低隨機性的作用。適用場景和「全抖動」一樣。

復制代碼
 
 
var baseNum = 2 ^ retryCount;
 
return baseNum + random(0 , baseNum);
   

3、4、5、6 策略的表現情況大致是這樣。(x 軸為重試次數)

99%的人都能看懂的「補償」以及最佳實踐

為什么說「重試」有坑呢?

正如前面聊到的那樣,出於對開發成本考慮,你在做「重試」的時候可能是復用的常規調用的接口。那么此時就不得不提一個「冪等性」問題。

如果實現「重試」選用的技術方案不能 100% 確保不會重復發起重試,那么「冪等性」問題是一個必須要考慮的問題。哪怕技術方案可以確保 100% 不會重復發起重試,出於對意外情況的考量,盡量也考慮一下「冪等性」問題。

冪等性:不管對程序發起幾次重復調用,程序表現的狀態(所有相關的數據變化)與調用一次的結果是一致的話,就是保證了冪等性。

這意味着可以根據需要重復或重試操作,而不會導致意外的影響。對於非冪等操作,算法可能必須跟蹤操作是否已經執行。

所以,一旦某個功能支持「重試」,那么整個鏈路上的接口都需要考慮冪等性問題,不能因為服務的多次調用而導致業務數據的累計增加或減少。

滿足「冪等性」其實就是需要想辦法識別重復的請求,並且將其過濾掉。思路就是:

  1. 給每個請求定義一個唯一標識。
  2. 在進行「重試」的時候判斷這個請求是否已經被執行或者正在被執行,如果是則拋棄該請求。

第 1 點,我們可以使用一個全局唯一 id 生成器或者生成服務。 或者簡單粗暴一些,使用官方類庫自帶的 Guid、uuid 之類的也行。

然后通過 rpc 框架在發起調用的客戶端中,對每個請求增加一個唯一標識的字段進行賦值。

第 2 點,我們可以在服務端通過 Aop 的方式切入到實際的處理邏輯代碼之前和之后,一起配合做驗證。

99%的人都能看懂的「補償」以及最佳實踐

大致的代碼思路如下。

【方法執行前】

復制代碼
 
 
if(isExistLog(requestId)){  //1. 判斷請求是否已被接收過。  對應序號 3
 
    var lastResult = getLastResult();  //2. 獲取用於判斷之前的請求是否已經處理完成。  對應序號 4
 
    if(lastResult == null){  
 
        var result = waitResult();  // 掛起等待處理完成
 
        return result;
 
   }
 
    else{
 
        return lastResult;
 
   }  
 
} else{
 
   log(requestId);   //3. 記錄該請求已接收
 
}
   
 
//do something..

【方法執行后】

復制代碼
 
 
log Result(requestId, result);  //4. 將結果也更新一下。

如果「補償」這個工作是通過 MQ 來進行的話,這事就可以直接在對接 MQ 所封裝的 SDK 中做。在生產端賦值全局唯一標識,在消費端通過唯一標識消重。

「重試」的最佳實踐

再聊一些最佳實踐吧,都是針對「重試」的,的確這也是工作中最常用的方案。

「重試」特別適合在高負載情況下被「降級」,當然也應當受到「限流」和「熔斷」機制的影響。當「重試」的“矛”與「限流」和「熔斷」的“盾”搭配使用,效果才是最好。

需要衡量增加補償機制的投入產出比。一些不是很重要的問題時,應該「快速失敗」而不是「重試」。

過度積極的重試策略(例如間隔太短或重試次數過多)會對下游服務造成不利影響,這點一定要注意。

一定要給「重試」制定一個終止策略。

當回滾的過程很困難或代價很大的情況下,可以接受很長的間隔及大量的重試次數,DDD 中經常被提到的「saga」模式其實也是這樣的思路。不過,前提是不會因為保留或鎖定稀缺資源而阻止其他操作(比如 1、2、3、4、5 幾個串行操作。由於 2 一直沒處理完成導致 3、4、5 沒法繼續進行)。

總結

這篇我們先聊了下做「補償」的意義,以及做補償的 2 個方式「回滾」和「重試」的實現思路。

然后,提醒你要注意「重試」的時候需要考慮冪等性問題,並且 z 哥也給出了一個解決思路。

最后,分享了幾個針對「重試」的最佳實踐。
希望對你有所幫助。

 

微服務架構:最終一致性 + 事務補償

分布式事務產生的原因

  • 數據庫分庫分表
  • 微服務化
  • 在微服務架構中,每個服務在用本地事務的時候,知道自己執行的事務是成功還是失敗,但是無法知道其他服務節點的事務執行情況,因此需要引入協調者TM,負責協調參與者RM的行為,並最終決定這些參與者是否把事務進行提交。

隨着微服務架構的流行,讓分布式事務問題日益突出, 那么常見的分布式事務解決方案有哪些呢? 如何理解最終一致性和它的事務補償機制呢?

剛性事務 - 強一致性

image.png

如上圖,這是個標准的全局事務,事務管理器控制着全局事務,管理事務的生命周期,並通過XA協議與資源管理器協調資源;資源管理器負責控制和管理實際的資源 (這里的資源管理器,可以是一個DBMS,或者消息服務管理系統)

兩階段提交

它是XA用於在全局事務中協調多個資源的機制,常用於事務管理器資源管理器之間,解決一致性問題,分兩階段:

  • 提交事務請求
  • 執行事務請求

image.png

2PC的問題

  • 效率低,與本地事務相比,XA協議的系統開銷比較大(數據被鎖定的時間跨度整個事務,直到全局事務的結束),只有支持XA協議的資源才能參與分布式事務。
  • 2PC是反可伸縮模式的,在事務處理過程中,參與者需要一直持有資源直到整個事務的結束,這樣當業務規模越來越大的情況下,它的局限性就越明顯。
  • 數據不一致,在2pc中的第二階段時,當TM向RM發送提交請求之后,發生局部的網絡異常或者在發送提交請求過程中TM發生故障, 這會導致只有一部分RM收到了提交請求,然后沒有收到提交請求的RM不會執行事務的提交,於是整個分布式系統便會出現數據不一致。
  • 單點故障, 由於TM的重要性,一旦發生故障,整個事務失效

3PC的改進

增加了超時機制, 主要解決單點故障問題,並減少資源鎖定時間,一旦RM無法及時收到來至TM的信息之后,它會默認執行Commit操作, 而不會一直持有事務資源並處於阻塞狀態。但是這種機制同樣會導致數據不一致的問題,由於網絡的原因,TM發送的回滾動作,沒有被RM及時的收到,那么RM等待超時后就執行了提交操作,這樣就和收到回滾操作並執行的RM之間存在了數據不一致的情況。

柔性事務 - 最終一致性

在2008年,eBay公布了基於BASE准則的最終一致性解決方案,它主要采用了消息隊列來輔助實現事務控制流程,其核心通過消息隊列的方式來異步執行分布式處理的任務,如果事務失敗,則可以發起人工重試的糾正流程(比如對賬系統,對處於dead letter queue的問題進行處理)

消息發送一致性

微服務架構下,需要通過網絡進行通信,就自然引入了數據傳輸的不確定性,也就是CAP原理中的P-分區容錯,而這里的消息發送一致性是可靠消息的保證。

生成消息的業務動作與消息發送的一致(e.g: 如果業務操作成功,那么由這個業務操作所產生的消息一定會成功投遞出去,否則就丟失消息)

最終一致性.png

如上圖,保證消息發送一致性的一般流程如下:

  • Producer先把消息發送給消息中間件服務,消息的狀態標記為待確認,這個狀態並不會被Consumer消費,對於長期待確認的消息,消息中間件會調用Producer的查詢接口,查看最新狀態,根據結果決定是否刪除消息。
  • Producer執行完業務操作后,向消息中間件服務,發送確認消息
  • 這時消息的狀態會被更改為待發送(可發送)
  • Consumer監聽並接收待發送狀態的消息,執行業務處理
  • Consumer業務處理后,向消息中間件服務發送ACK,確認消息已經收到(消息中間件服務將從隊列中刪除該消息)

消息的ACK確認流程中,任何一個環節都可能會出問題!

未ACK的消息,采用按規則重新投遞的方式進行處理(很多MQ都提供at least once的投遞,持久化和重試機制),一般還會設置重發的次數, 超過次數的消息會進入dead letter queue,等待人工干預或者延后定時處理。

業務接口的冪等性

消息的重復發送會導致業務接口出現重復調用的問題,主要原因就是消息沒有及時收到ACK確認導致的, 那如何實現冪等性設計呢?

在實際的業務場景中, 業務接口的冪等性設計,常結合查詢操作一起使用,

比如根據唯一標識查詢消息是否被處理過, 或者根據消費日志表,來維護消息消費的記錄。

保證最終一致性的模式

  • 可查詢模式,任何一個服務操作都提供一個可查詢接口,用來向外部輸出操作執行的狀態,下游Consumer可以通過接口得知服務操作執行的狀態,然后根據不同的狀態做不同的處理操作(執行或者取消), 該模式對業務接口有一定侵入性。
  • 補償模式, 有了查詢模式,我們能夠知道操作的具體狀態,如果處於不正常狀態,我們可以修正操作中出現的問題,或許是重新執行,或許取消已經完成的操作,通過修復是的整個分布式系統達到最終一致。
  • 最大努力通知模式, 在調用支付寶交易接口或微信支付接口時,一般會在回調頁面和接口里,解密參數,然后調用系統中更新交易狀態相關的服務,將訂單更新為付款成功。同時,只有當回調頁面中輸出了success字樣或者標識業務處理成功相應狀態碼時,支付寶才會停止回調請求。否則,支付寶會每間隔一段時間后,再向客戶方發起回調請求,直到輸出成功標識為止。

 

簡單聊聊消息隊列的事務補償機制

 

因為一直學習與嘗試負責公司的推送相關業務,包括整個應用的實現,其中就采用了基於消息隊列的異步事件驅動模型來做解耦異步處理,所以就要去做了解一些相關的知識點,這邊稍作總結,並整理一下消息補償機制的一套簡單實現的代碼設計圖。

采用基於消息隊列的異步事件驅動模型來解決問題的時候,一個計較棘手的問題就是事務的一致性。

案例:現在用戶發起一個創建訂單的請求,如果我們是單系統架構,那么修改訂單表,修改庫存表可能都是在同一個事務中完成,所以輕而易舉就達到了事物一致性原則,但是這不是我們要討論的,所以就帶過。現在微服務架構在互聯網公司大火特火,熱度未減,分布式是事務也成為了一個亟待解決的問題,阿里雲GTS標榜如何讓分布式事務更簡單。

比如,用戶發起一個創建訂單的請求,首先在訂單服務上生成了新的訂單,同時還要去庫存服務中減去庫存,因為是分布式架構,所以庫存扣減與訂單創建可能是在兩個遙遠的機器上,如果想要通過本地事務來解決那幾乎是不可能的,保證兩個事務之間的狀態一致性——訂單創建成功,庫存扣減失敗,如何回滾訂單?一直都是分布式架構中繞不開的挑戰。

分布式架構中如何解決事務問題,在很多技術群都上都在討論,比如dubbo , spring cloud等等。目前還沒有接觸到這方面相關知識,后續如果有幸參與,可做分享,本次想要聊的是假基於消息隊列的異步事件驅動是如何解決如上的分布式問題,以及如何保證事務一致性。

事務一致性原則(ACID):

  • Atomicity - 原子性,改變數據狀態要么是一起完成,要么一起失敗

  • Consistency - 一致性,數據的狀態是完整一致的

  • Isolation - 隔離線,即使有並發事務,互相之間也不影響

  • Durability - 持久性, 一旦事務提交,不可撤銷

訂單創建完成之后,發送一個createOrderEvent到消息隊列中,由消息隊列負責轉發給訂閱該消息的消費者進行處理。

好,這個時候如果消息消費 成功,但是庫存不足,庫存扣減失敗,訂單創建則不能成功,這個時候很好處理,由庫存服務推送一個subInventoryFail給到訂單服務,訂單服務根據消息將訂單轉為失敗狀態。

1、從用戶體驗的角度來說,整個過程是異步的,所以對於用戶的體驗來說,就做不到“立馬成功或立馬失敗”的效果。

2、從技術的角度來說,整個過程你不再關注同一個事物的問題,而是關注最終訂單的狀態是否一致。【注:從分布式事務<-->最終一致性】保證事務最終一致性,但是基於這種事件驅動達到最終一致,解耦事務的成功實施需要依賴幾個因素。

a、消息的投遞是否可靠。

b、消息的可靠性,例如訂單服務已經成功創建訂單,但是還沒來得及發送消息就宕機或者各種原因,導致訂單的狀態不一致。

基於以上兩點的考慮,我們使用了一種基於本地事務的方案來保證消息最終的一致性。

創建訂單與創建消息事件都在本地事務中,屬於同一個事務,可以保證訂單表與消息事件表的數據一致性。發送消息到消息中間件,在事務提交之后發送。到了庫存服務的時候,啟動一個定時任務去掃描消息事件表,將未投遞失敗/消費 失敗的消息進行消費,即補償事務一致性。

定時任務的方案可能不是最佳的,可以稍作改定,比如采用阿里巴巴開源的Canal

公司目前也是采用這種架構來解決訂單與庫存問題。有網友的做法是保證消息投遞的可靠性,我們則是保證消費的一致性,具體的文章點我>>

可以將消息隊列的進行封裝,做成了一個starter,代碼設計上大致如圖下:

@山茶果


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM