1 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看起來很有希望滿足我們的需求。所有長活事務都可以這樣做嗎?這里有一些限制:
- Saga只允許兩個層次的嵌套,頂級的Saga和簡單子事務
- 在外層,全原子性不能得到滿足。也就是說,sagas可能會看到其他sagas的部分結果
- 每個子事務應該是獨立的原子行為
- 在我們的業務場景下,各個業務環境(如:航班預訂、租車、酒店預訂和付款)是自然獨立的行為,而且每個事務都可以用對應服務的數據庫保證原子操作。
補償也有需考慮的事項:
- 補償事務從語義角度撤消了事務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有以下這些要求:
- Ti和Ci是冪等的。
- Ci必須是能夠成功的,如果無法成功則需要人工介入。
- 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,那么就會有三種情況:
- Ti的請求丟失了,服務之前沒有、之后也不會執行Ti
- Ti在Ci之前執行
- 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的路徑如下:
- Order Service在APPROVAL_PENDING狀態下創建一個Order並發布OrderCreated事件。
- Consumer Service消費OrderCreated事件,驗證消費者是否可以下訂單,並發布ConsumerVerified事件。
- Kitchen Service消費OrderCreated事件,驗證訂單,在CREATE_PENDING狀態下創建故障單,並發布TicketCreated事件。
- Accounting服務消費OrderCreated事件並創建一個處於PENDING狀態的Credit CardAuthorization。
- Accounting Service消費TicketCreated和ConsumerVerified事件,收取消費者的信用卡,並發布信用卡授權活動。
- Kitchen Service使用CreditCardAuthorized事件並更改AWAITING_ACCEPTANCE票的狀態。
- Order Service收到CreditCardAuthorized事件,更改訂單狀態到APPROVED,並發布OrderApproved事件。
創建訂單saga還必須處理saga參與者拒絕訂單並發布某種失敗事件的場景。例如,消費者信用卡的授權可能會失敗。saga必須執行補償交易以撤消已經完成的事情。圖中顯示了AccountingService無法授權消費者信用卡時的事件流。

事件順序如下:
- Order服務在APPROVAL_PENDING狀態下創建一個Order並發布OrderCreated事件。
- Consumer服務消費OrderCreated事件,驗證消費者是否可以下訂單,並發布ConsumerVerified事件。
- Kitchen服務消費OrderCreated事件,驗證訂單,在CREATE_PENDING狀態下創建故障單,並發布TicketCreated事件。
- Accounting服務消費OrderCreated事件並創建一個處於PENDING狀態的Credit CardAuthorization。
- Accounting服務消費TicketCreated和ConsumerVerified事件,向消費者的信用卡收費,並發布信用卡授權失敗事件。
- Kitchen服務使用信用卡授權失敗事件並將故障單的狀態更改為REJECTED。
- 訂單服務消費信用卡授權失敗事件,並將訂單狀態更改為已拒絕。
可靠的基於事件的通信
在實施基於編排的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有幾個好處:
- 更簡單的依賴關系:編排的一個好處是它不會引入循環依賴關系。 saga orchestrator調用saga參與者,但參與者不會調用orchestrator。因此,協調器依賴於參與者,但反之亦然,因此沒有循環依賴性。
- 較少的耦合:每個服務都實現了由orchestrator調用的API,因此它不需要知道saga參與者發布的事件。
- 改善關注點分離並簡化業務邏輯:saga協調邏輯本地化在saga協調器中。域對象更簡單,並且不了解它們參與的saga。例如,當使用編排時,Order類不知道任何saga,因此它具有更簡單的狀態機模型。在執行創建訂單saga期間,它直接從APPROVAL_PENDING狀態轉換到APPROVED狀態。 Order類沒有與saga的步驟相對應的任何中間狀態。因此,業務更加簡單。
業務流程也有一個缺點:
- 在協調器中集中過多業務邏輯的風險。這導致了一種設計,其中智能協調器告訴啞巴服務要做什么操作。幸運的是,您可以通過設計獨立負責排序的協調器來避免此問題,並且不包含任何其他業務邏輯。
除了最簡單的saga,我建議使用編排。為您的saga實施協調邏輯只是您需要解決的設計問題之一。