分布式事務:Saga模式


1 Saga相關概念

1987年普林斯頓大學的Hector Garcia-Molina和Kenneth Salem發表了一篇Paper Sagas,講述的是如何處理long lived transaction(長活事務)。Saga是一個長活事務可被分解成可以交錯運行的子事務集合。其中每個子事務都是一個保持數據庫一致性的真實事務。
論文地址: sagas

1.1 Saga的組成

  • 每個Saga由一系列sub-transaction Ti 組成
  • 每個Ti 都有對應的補償動作Ci,補償動作用於撤銷Ti造成的結果

可以看到,和TCC相比,Saga沒有“預留”動作,它的Ti就是直接提交到庫。

Saga的執行順序有兩種:

  • T1, T2, T3, ..., Tn
  • T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n

Saga定義了兩種恢復策略:

  • backward recovery,向后恢復,補償所有已完成的事務,如果任一子事務失敗。即上面提到的第二種執行順序,其中j是發生錯誤的sub-transaction,這種做法的效果是撤銷掉之前所有成功的sub-transation,使得整個Saga的執行結果撤銷。
  • forward recovery,向前恢復,重試失敗的事務,假設每個子事務最終都會成功。適用於必須要成功的場景,執行順序是類似於這樣的:T1, T2, ..., Tj(失敗), Tj(重試),..., Tn,其中j是發生錯誤的sub-transaction。該情況下不需要Ci

顯然,向前恢復沒有必要提供補償事務,如果你的業務中,子事務(最終)總會成功,或補償事務難以定義或不可能,向前恢復更符合你的需求。

理論上補償事務永不失敗,然而,在分布式世界中,服務器可能會宕機,網絡可能會失敗,甚至數據中心也可能會停電。在這種情況下我們能做些什么? 最后的手段是提供回退措施,比如人工干預。

1.2 Saga的使用條件

Saga看起來很有希望滿足我們的需求。所有長活事務都可以這樣做嗎?這里有一些限制:

  1. Saga只允許兩個層次的嵌套,頂級的Saga和簡單子事務
  2. 在外層,全原子性不能得到滿足。也就是說,sagas可能會看到其他sagas的部分結果
  3. 每個子事務應該是獨立的原子行為
  4. 在我們的業務場景下,各個業務環境(如:航班預訂、租車、酒店預訂和付款)是自然獨立的行為,而且每個事務都可以用對應服務的數據庫保證原子操作。

補償也有需考慮的事項:

  • 補償事務從語義角度撤消了事務Ti的行為,但未必能將數據庫返回到執行Ti時的狀態。(例如,如果事務觸發導彈發射, 則可能無法撤消此操作)

但這對我們的業務來說不是問題。其實難以撤消的行為也有可能被補償。例如,發送電郵的事務可以通過發送解釋問題的另一封電郵來補償。

對於ACID的保證:

Saga對於ACID的保證和TCC一樣:

  • 原子性(Atomicity):正常情況下保證。
  • 一致性(Consistency),在某個時間點,會出現A庫和B庫的數據違反一致性要求的情況,但是最終是一致的。
  • 隔離性(Isolation),在某個時間點,A事務能夠讀到B事務部分提交的結果。
  • 持久性(Durability),和本地事務一樣,只要commit則數據被持久。

Saga不提供ACID保證,因為原子性和隔離性不能得到滿足。原論文描述如下:

full atomicity is not provided. That is, sagas may view the partial results of other sagas

通過saga log,saga可以保證一致性和持久性。


和TCC對比

Saga相比TCC的缺點是缺少預留動作,導致補償動作的實現比較麻煩:Ti就是commit,比如一個業務是發送郵件,在TCC模式下,先保存草稿(Try)再發送(Confirm),撤銷的話直接刪除草稿(Cancel)就行了。而Saga則就直接發送郵件了(Ti),如果要撤銷則得再發送一份郵件說明撤銷(Ci),實現起來有一些麻煩。

如果把上面的發郵件的例子換成:A服務在完成Ti后立即發送Event到ESB(企業服務總線,可以認為是一個消息中間件),下游服務監聽到這個Event做自己的一些工作然后再發送Event到ESB,如果A服務執行補償動作Ci,那么整個補償動作的層級就很深。

不過沒有預留動作也可以認為是優點:

  • 有些業務很簡單,套用TCC需要修改原來的業務邏輯,而Saga只需要添加一個補償動作就行了。
  • TCC最少通信次數為2n,而Saga為n(n=sub-transaction的數量)。
  • 有些第三方服務沒有Try接口,TCC模式實現起來就比較tricky了,而Saga則很簡單。
  • 沒有預留動作就意味着不必擔心資源釋放的問題,異常處理起來也更簡單(請對比Saga的恢復策略和TCC的異常處理)。

2 Saga相關實現

 

Saga Log

Saga保證所有的子事務都得以完成或補償,但Saga系統本身也可能會崩潰。Saga崩潰時可能處於以下幾個狀態:

  • Saga收到事務請求,但尚未開始。因子事務對應的微服務狀態未被Saga修改,我們什么也不需要做。
  • 一些子事務已經完成。重啟后,Saga必須接着上次完成的事務恢復。
  • 子事務已開始,但尚未完成。由於遠程服務可能已完成事務,也可能事務失敗,甚至服務請求超時,saga只能重新發起之前未確認完成的子事務。這意味着子事務必須冪等。
  • 子事務失敗,其補償事務尚未開始。Saga必須在重啟后執行對應補償事務。
  • 補償事務已開始但尚未完成。解決方案與上一個相同。這意味着補償事務也必須是冪等的。
  • 所有子事務或補償事務均已完成,與第一種情況相同。

為了恢復到上述狀態,我們必須追蹤子事務及補償事務的每一步。我們決定通過事件的方式達到以上要求,並將以下事件保存在名為saga log的持久存儲中:

  • Saga started event 保存整個saga請求,其中包括多個事務/補償請求
  • Transaction started event 保存對應事務請求
  • Transaction ended event 保存對應事務請求及其回復
  • Transaction aborted event 保存對應事務請求和失敗的原因
  • Transaction compensated event 保存對應補償請求及其回復
  • Saga ended event 標志着saga事務請求的結束,不需要保存任何內容

 

 

通過將這些事件持久化在saga log中,我們可以將saga恢復到上述任何狀態。

由於Saga只需要做事件的持久化,而事件內容以JSON的形式存儲,Saga log的實現非常靈活,數據庫(SQL或NoSQL),持久消息隊列,甚至普通文件可以用作事件存儲, 當然有些能更快得幫saga恢復狀態。

注意事項

對於服務來說,實現Saga有以下這些要求:

  1. Ti和Ci是冪等的。
  2. Ci必須是能夠成功的,如果無法成功則需要人工介入。
  3. Ti - Ci和Ci - Ti的執行結果必須是一樣的:sub-transaction被撤銷了。

第一點要求Ti和Ci是冪等的,舉個例子,假設在執行Ti的時候超時了,此時我們是不知道執行結果的,如果采用forward recovery策略就會再次發送Ti,那么就有可能出現Ti被執行了兩次,所以要求Ti冪等。如果采用backward recovery策略就會發送Ci,而如果Ci也超時了,就會嘗試再次發送Ci,那么就有可能出現Ci被執行兩次,所以要求Ci冪等。

第二點要求Ci必須能夠成功,這個很好理解,因為,如果Ci不能執行成功就意味着整個Saga無法完全撤銷,這個是不允許的。但總會出現一些特殊情況比如Ci的代碼有bug、服務長時間崩潰等,這個時候就需要人工介入了。

第三點乍看起來比較奇怪,舉例說明,還是考慮Ti執行超時的場景,我們采用了backward recovery,發送一個Ci,那么就會有三種情況:

  1. Ti的請求丟失了,服務之前沒有、之后也不會執行Ti
  2. Ti在Ci之前執行
  3. Ci在Ti之前執行

對於第1種情況,容易處理。對於第2、3種情況,則要求Ti和Ci是可交換的(commutative),並且其最終結果都是sub-transaction被撤銷。


3 Saga協調

協調saga:saga的實現包含協調saga步驟的邏輯。當系統命令啟動saga時,協調邏輯必須選擇並告知第一個saga參與者執行本地事務。一旦該事務完成,saga的排序協調選擇並調用下一個saga參與者。這個過程一直持續到saga執行了所有步驟。如果任何本地事務失敗,則saga必須以相反的順序執行補償事務。構建一個saga的協調邏輯有幾種不同的方法:

  • 編排(Choreography):在saga參與者中分配決策和排序。他們主要通過交換事件進行溝通。
  • 控制(Orchestration):在saga控制類中集中saga的協調邏輯。一個saga控制者向saga參與者發送命令消息,告訴他們要執行哪些操作。

3.1 編排(Choreography)

基於編排的saga:實現sagas的一種方法是使用編排。當使用編排時,沒有中央協調員告訴saga參與者該做什么。相反,sagas參與者訂閱彼此的事件並做出相應的響應。


通過這個sagas的路徑如下:

  1. Order Service在APPROVAL_PENDING狀態下創建一個Order並發布OrderCreated事件。
  2. Consumer Service消費OrderCreated事件,驗證消費者是否可以下訂單,並發布ConsumerVerified事件。
  3. Kitchen Service消費OrderCreated事件,驗證訂單,在CREATE_PENDING狀態下創建故障單,並發布TicketCreated事件。
  4. Accounting服務消費OrderCreated事件並創建一個處於PENDING狀態的Credit CardAuthorization。
  5. Accounting Service消費TicketCreated和ConsumerVerified事件,收取消費者的信用卡,並發布信用卡授權活動。
  6. Kitchen Service使用CreditCardAuthorized事件並更改AWAITING_ACCEPTANCE票的狀態。
  7. Order Service收到CreditCardAuthorized事件,更改訂單狀態到APPROVED,並發布OrderApproved事件。

創建訂單saga還必須處理saga參與者拒絕訂單並發布某種失敗事件的場景。例如,消費者信用卡的授權可能會失敗。saga必須執行補償交易以撤消已經完成的事情。圖中顯示了AccountingService無法授權消費者信用卡時的事件流。

 

 

 

事件順序如下:

  1. Order服務在APPROVAL_PENDING狀態下創建一個Order並發布OrderCreated事件。
  2. Consumer服務消費OrderCreated事件,驗證消費者是否可以下訂單,並發布ConsumerVerified事件。
  3. Kitchen服務消費OrderCreated事件,驗證訂單,在CREATE_PENDING狀態下創建故障單,並發布TicketCreated事件。
  4. Accounting服務消費OrderCreated事件並創建一個處於PENDING狀態的Credit CardAuthorization。
  5. Accounting服務消費TicketCreated和ConsumerVerified事件,向消費者的信用卡收費,並發布信用卡授權失敗事件。
  6. Kitchen服務使用信用卡授權失敗事件並將故障單的狀態更改為REJECTED。
  7. 訂單服務消費信用卡授權失敗事件,並將訂單狀態更改為已拒絕。

可靠的基於事件的通信

在實施基於編排的saga時,您必須考慮一些與服務間通信相關的問題。第一個問題是確保saga參與者更新其數據庫並將事件作為數據庫事務的一部分發布。
您需要考慮的第二個問題是確保saga參與者必須能夠將收到的每個事件映射到自己的數據。

編組的saga的好處和缺點

基於編舞的saga有幾個好處

  • 簡單:服務在創建,更新或刪除業務時發布事件對象
  • 松耦合:參與者訂閱事件並且彼此之間沒有直接的了解。

並且有一些缺點

  • 更難理解:與業務流程不同,代碼中沒有一個地方可以定義saga。相反,編排在服務中分配saga的實現。因此,開發人員有時很難理解給定的saga是如何工作的。
  • 服務之間的循環依賴關系:saga參與者訂閱彼此的事件,這通常會創建循環依賴關系。例如,如果仔細檢查圖示,您將看到存在循環依賴關系,例如訂單服務、會計服務、訂單服務。雖然這不一定是個問題,但循環依賴性被認為是設計問題。
  • 緊密耦合的風險:每個saga參與者都需要訂閱所有影響他們的事件。例如,會計服務必須訂閱導致消費者信用卡被收費或退款的所有事件。因此,存在一種風險,即需要與Order Service實施的訂單生命周期保持同步更新。

3.2 控制(Orchestration)

控制是實現sagas的另一種方式。使用業務流程時,您可以定義一個控制類,其唯一的職責是告訴saga參與者該做什么。 saga控制使用命令/異步回復樣式交互與參與者進行通信。

 

 

  • Order Service首先創建一個Order和一個創建訂單控制器。之后,路徑的流程如下:
  • saga orchestrator向Consumer Service發送Verify Consumer命令。
  • Consumer Service回復Consumer Verified消息。
  • saga orchestrator向Kitchen Service發送Create Ticket命令。
  • Kitchen Service回復Ticket Created消息。
  • saga協調器向Accounting Service發送授權卡消息。
  • Accounting服務部門使用卡片授權消息回復。
  • saga orchestrator向Kitchen Service發送Approve Ticket命令。
  • saga orchestrator向訂單服務發送批准訂單命令。

使用狀態機建模SAGA ORCHESTRATORS

建模saga orchestrator的好方法是作為狀態機。狀態機由一組狀態和一組由事件觸發的狀態之間的轉換組成。每個transition都可以有一個action,對於一個saga來說是一個saga參與者的調用。狀態之間的轉換由saga參與者執行的本地事務的完成觸發。當前狀態和本地事務的特定結果決定了狀態轉換以及執行的操作(如果有的話)。對狀態機也有有效的測試策略。因此,使用狀態機模型可以更輕松地設計、實施和測試。

 

 

圖顯示了Create Order Saga的狀態機模型。此狀態機由多個狀態組成,包括以下內容:

  • Verifying Consumer:初始狀態。當處於此狀態時,該saga正在等待消費者服務部門驗證消費者是否可以下訂單。
  • Creating Ticket:該saga正在等待對創建票證命令的回復。
  • Authorizing Card:等待Accounting服務授權消費者的信用卡。
  • OrderApproved:表示saga成功完成的最終狀態。
  • Order Rejected:最終狀態表明該訂單被其中一方參與者們拒絕。

SAGA ORCHESTRATION和TRANSACTIONAL MESSAGING

基於業務流程的saga的每個步驟都包括更新數據庫和發布消息的服務。例如,Order Service持久保存Order和Create Order Saga orchestrator,並向第一個saga參與者發送消息。一個saga參與者,例如Kitchen Service,通過更新其數據庫並發送回復消息來處理命令消息。 Order Service通過更新saga協調器的狀態並向下一個saga參與者發送命令消息來處理參與者的回復消息。服務必須使用事務性消息傳遞,以便自動更新數據庫並發布消息。

讓我們來看看使用saga編排的好處和缺點。

基於ORCHESTRATION的SAGAS的好處和缺點

基於編排的saga有幾個好處:

  1. 更簡單的依賴關系:編排的一個好處是它不會引入循環依賴關系。 saga orchestrator調用saga參與者,但參與者不會調用orchestrator。因此,協調器依賴於參與者,但反之亦然,因此沒有循環依賴性。
  2. 較少的耦合:每個服務都實現了由orchestrator調用的API,因此它不需要知道saga參與者發布的事件。
  3. 改善關注點分離並簡化業務邏輯:saga協調邏輯本地化在saga協調器中。域對象更簡單,並且不了解它們參與的saga。例如,當使用編排時,Order類不知道任何saga,因此它具有更簡單的狀態機模型。在執行創建訂單saga期間,它直接從APPROVAL_PENDING狀態轉換到APPROVED狀態。 Order類沒有與saga的步驟相對應的任何中間狀態。因此,業務更加簡單。

業務流程也有一個缺點

  • 在協調器中集中過多業務邏輯的風險。這導致了一種設計,其中智能協調器告訴啞巴服務要做什么操作。幸運的是,您可以通過設計獨立負責排序的協調器來避免此問題,並且不包含任何其他業務邏輯。

除了最簡單的saga,我建議使用編排。為您的saga實施協調邏輯只是您需要解決的設計問題之一。



 





 

 

 





 




免責聲明!

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



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