基於 Seata Saga 設計更有彈性的金融應用
- 2019 年 11 月 28 日
- 筆記
Seata 意為:Simple Extensible Autonomous Transaction Architecture,是一套一站式分散式事務解決方案,提供了 AT、TCC、Saga 和 XA 事務模式,本文詳解其中的 Saga 模式。
項目地址:https://github.com/seata/seata
本文作者:屹遠(陳龍),螞蟻金服分散式事務核心研發 。
金融分散式應用開發的痛點
分散式系統有一個比較明顯的問題就是,一個業務流程需要組合一組服務。這樣的事情在微服務下就更為明顯了,因為這需要業務上的一致性的保證。也就是說,如果一個步驟失敗了,那麼要麼回滾到以前的服務調用,要麼不斷重試保證所有的步驟都成功。—《左耳聽風-彈力設計之「補償事務」》
而在金融領域微服務架構下的業務流程往往會更複雜,流程很長,比如一個互聯網微貸業務流程調十幾個服務很正常,再加上異常處理的流程那就更複雜了,做過金融業務開發的同學會很有體感。
所以在金融分散式應用開發過程中我們面臨一些痛點:
- 業務一致性難以保障
我們接觸到的大多數業務(比如在渠道層、產品層、集成層的系統),為了保障業務最終一致性,往往會採用「補償」的方式來做,如果沒有一個協調器來支援,開發難度是比較大的,每一步都要在 catch 里去處理前面所有的「回滾」操作,這將會形成「箭頭形」的程式碼,可讀性及維護性差。或者重試異常的操作,如果重試不成功可能要轉非同步重試,甚至最後轉人工處理。這些都給開發人員帶來極大的負擔,開發效率低,且容易出錯。
- 業務狀態難以管理
業務實體很多、實體的狀態也很多,往往做完一個業務活動後就將實體的狀態更新到了資料庫里,沒有一個狀態機來管理整個狀態的變遷過程,不直觀,容易出錯,造成業務進入一個不正確的狀態。
- 冪等性難以保障
服務的冪等性是分散式環境下的基本要求,為了保證服務的冪等性往往需要服務開發者逐個去設計,有用資料庫唯一鍵實現的,有用分散式快取實現的,沒有一個統一的方案,開發人員負擔大,也容易遺漏,從而造成資損。
- 業務監控運維難,缺乏統一的差錯守護能力
業務的執行情況監控一般通過列印日誌,再基於日誌監控平台查看,大多數情況是沒有問題的,但是如果業務出錯,這些監控缺乏當時的業務上下文,對排查問題不友好,往往需要再去資料庫里查。同時日誌的列印也依賴於開發,容易遺漏。對於補償事務往往需要有「差錯守護觸發補償」、「工人觸發補償」操作,沒有統一的差錯守護和處理規範,這些都要開發者逐個開發,負擔沉重。
理論基礎
一些場景下,我們對數據有強一致性的需求時,會採用在業務層上需要使用「兩階段提交」這樣的分散式事務方案。而在另外一些場景下,我們並不需要這麼強的一致性,那就只需要保證最終一致性就可以了。
例如螞蟻金服目前在金融核心系統使用的就是 TCC 模式,金融核心系統的特點是一致性要求高(業務上的隔離性)、短流程、並發高。
而在很多金融核心以上的業務(比如在渠道層、產品層、集成層的系統),這些系統的特點是最終一致即可、流程多、流程長、還可能要調用其它公司的服務(如金融網路)。這是如果每個服務都開發 Try、Confirm、Cancel 三個方法成本高。如果事務中有其它公司的服務,也無法要求其它公司的服務也遵循 TCC 這種開發模式。同時流程長,事務邊界太長會影響性能。
對於事務我們都知道 ACID,也很熟悉 CAP 理論最多只能滿足其中兩個,所以,為了提高性能,出現了 ACID 的一個變種 BASE。ACID 強調的是一致性(CAP 中的 C),而 BASE 強調的是可用性(CAP 中的 A)。我們知道,在很多情況下,我們是無法做到強一致性的 ACID 的。特別是我們需要跨多個系統的時候,而且這些系統還不是由一個公司所提供的。BASE 的系統傾向於設計出更加有彈力的系統,在短時間內,就算是有數據不同步的風險,我們也應該允許新的交易可以發生,而後面我們在業務上將可能出現問題的事務通過補償的方式處理掉,以保證最終的一致性。
所以我們在實際開發中會進行取捨,對於更多的金融核心以上的業務系統可以採用補償事務,補償事務處理方面在30年前就提出了 Saga 理論,隨著微服務的發展,近些年才逐步受到大家的關注。目前業界比較也公認 Saga 是作為長事務的解決方案。
https://github.com/aphyr/dist-sagas/blob/master/sagas.pdf[1] http://microservices.io/patterns/data/saga.html[2]
社區和業界的方案
Apache Camel Saga
Camel 是實現 EIP(Enterprise Integration Patterns)企業集成模式的一款開源產品,它基於事件驅動的架構,有著良好的性能和吞吐量,它在2.21版本新增加了 Saga EIP。
Saga EIP 提供了一種方式可以通過 camel route 定義一系列有關聯關係的 Action,這些 Action 要麼都執行成功,要麼都回滾,Saga 可以協調任何通訊協議的分散式服務或本地服務,並達到全局的最終一致性。Saga 不要求整個處理在短時間內完成,因為它不佔用任何資料庫鎖,它可以支援需要長時間處理的請求,從幾秒到幾天,Camel 的 Saga EIP 是基於 Microprofile 的 LRA[3](Long Running Action),同樣也是支援協調任何通訊協議任何語言實現的分散式服務。
Saga 的實現不會對數據進行加鎖,而是在給操作定義它的「補償操作」,當正常流程執行出錯的時候觸發那些已經執行過的操作的「補償操作」,將流程回滾掉。「補償操作」可以在 Camel route 上用 Java 或 XML DSL(Definition Specific Language)來定義。
下面是一個 Java DSL 示例:

XML DSL 示例:

Eventuate Tram Saga
Eventuate Tram Saga[4] 框架是使用 JDBC / JPA 的 Java 微服務的一個 Saga 框架。它也和 Camel Saga 一樣採用了 Java DSL 來定義補償操作:

Apache ServiceComb Saga
ServiceComb Saga[5] 也是一個微服務應用的數據最終一致性解決方案。相對於 TCC 而言,在 try 階段,Saga 會直接提交事務,後續 rollback 階段則通過反向的補償操作來完成。與前面兩種不同是它是採用 Java 註解+攔截器的方式來進行「補償」服務的定義。
架構:
Saga 是由 alpha 和 omega 組成,其中:
- alpha 充當協調者的角色,主要負責對事務進行管理和協調;
- omega 是微服務中內嵌的一個 agent,負責對網路請求進行攔截並向 alpha 上報事務事件;
下圖展示了 alpha,omega 以及微服務三者的關係:

使用示例:

螞蟻金服的實踐
螞蟻金服內部大規模在使用 TCC 模式分散式事務,主要用於金融核心等對一致性要求高、性能要求高的場景。在更上層的業務系統因為流程多流程長,開發 TCC 成本比較高,大都會權衡採用 Saga 模式來到達業務最終一致性,由於歷史的原因不同的 BU 有自己的一套「補償」事務的方案,基本上是兩種:
- 一種是當一個服務在失敗時需要「重試」或「補償」時,在執行服務前在資料庫插入一條記錄,記錄狀態,當異常時通過定時任務去查詢資料庫記錄並進行「重試」或「補償」,當業務流程執行成功則刪除記錄;
- 另一種是設計一個狀態機引擎和簡單的 DSL,編排業務流程和記錄業務狀態,狀態機引擎可以定義「補償服務」,當異常時由狀態機引擎反向調用「補償服務」進行回滾,同時還會有一個「差錯守護」平台,監控那些執行失敗或補償失敗的業務流水,並不斷進行「補償」或「重試」;
方案對比
社區和業界的解決方案一般是兩種,一種基本狀態機或流程引擎通過 DSL 方式編排流程程和補償定義,一種是基於 Java 註解+攔截器實現補償,那麼這兩種方案有什麼優缺點呢?
方式 |
優點 |
缺點 |
---|---|---|
狀態機+DSL |
1. 可以用可視化工具來定義業務流程,標準化,可讀性高,可實現服務編排的功能2. 提高業務分析人員與程式開發人員的溝通效率3. 業務狀態管理:流程本質就是一個狀態機,可以很好的反映業務狀態的流轉4. 提高異常處理靈活性:可以實現宕機恢復後的「向前重試」或「向後補償」5. 天然可以使用 Actor 模型或 SEDA 架構等非同步處理引擎來執行,提高整體吞吐 |
1. 業務流程實際是由 JAVA 程式與 DSL 配置組成,程式與配置分離,開發起來比較繁瑣2. 如果是改造現有業務,對業務侵入性高3. 引擎實現成本高 |
攔截器+java 註解 |
1. 程式與註解是在一起的,開發簡單,學習成本低2. 方便接入現有業務3. 基於動態代理攔截器,框架實現成本 |
1. 框架無法提供 Actor 模型或 SEDA 架構等非同步處理模式來提高系統吞吐量2. 框架無法提供業務狀態管理3. 難以實現宕機恢復後的「向前重試」,因為無法恢復執行緒上下文 |
Seata Saga 的方案
Seata Saga 的簡介可以看一下《Seata Saga 官網文檔》[6]。
Seata Saga 採用了狀態機+DSL 方案來實現,原因有以下幾個:
- 狀態機+DSL 方案在實際生產中應用更廣泛;
- 可以使用 Actor 模型或 SEDA 架構等非同步處理引擎來執行,提高整體吞吐量;
- 通常在核心系統以上層的業務系統會伴隨有「服務編排」的需求,而服務編排又有事務最終一致性要求,兩者很難分割開,狀態機+DSL 方案可以同時滿足這兩個需求;
- 由於 Saga 模式在理論上是不保證隔離性的,在極端情況下可能由於臟寫無法完成回滾操作,比如舉一個極端的例子, 分散式事務內先給用戶 A 充值,然後給用戶 B 扣減餘額,如果在給A用戶充值成功,在事務提交以前,A 用戶把線消費掉了,如果事務發生回滾,這時則沒有辦法進行補償了,有些業務場景可以允許讓業務最終成功,在回滾不了的情況下可以繼續重試完成後面的流程,狀態機+DSL的方案可以實現「向前」恢復上下文繼續執行的能力, 讓業務最終執行成功,達到最終一致性的目的。
在不保證隔離性的情況下:業務流程設計時要遵循「寧可長款, 不可短款」的原則,長款意思是客戶少了線機構多了錢,以機構信譽可以給客戶退款,反之則是短款,少的線可能追不回來了。所以在業務流程設計上一定是先扣款。
狀態定義語言(Seata State Language)
- 通過狀態圖來定義服務調用的流程並生成 json 狀態語言定義文件;
- 狀態圖中一個節點可以是調用一個服務,節點可以配置它的補償節點;
- 狀態圖 json 由狀態機引擎驅動執行,當出現異常時狀態引擎反向執行已成功節點對應的補償節點將事務回滾;注意: 異常發生時是否進行補償也可由用戶自定義決定
- 可以實現服務編排需求,支援單項選擇、並發、非同步、子狀態機、參數轉換、參數映射、服務執行狀態判斷、異常捕獲等功能;
假設有一個業務流程要調兩個服務,先調庫存扣減(InventoryService),再調餘額扣減(BalanceService),保證在一個分散式內要麼同時成功,要麼同時回滾。兩個參與者服務都有一個 reduce 方法,表示庫存扣減或餘額扣減,還有一個 compensateReduce 方法,表示補償扣減操作。以 InventoryService 為例看一下它的介面定義:

這個業務流程對應的狀態圖:

對應的 JSON:

狀態語言在一定程度上參考了 AWS Step Functions[7]。
"狀態機" 屬性簡介:
- Name: 表示狀態機的名稱,必須唯一;
- Comment: 狀態機的描述;
- Version: 狀態機定義版本;
- StartState: 啟動時運行的第一個"狀態";
- States: 狀態列表,是一個 map 結構,key 是"狀態"的名稱,在狀態機內必須唯一;
"狀態" 屬性簡介:
- Type:"狀態" 的類型,比如有:
- ServiceTask: 執行調用服務任務;
- Choice: 單條件選擇路由;
- CompensationTrigger: 觸發補償流程;
- Succeed: 狀態機正常結束;
- Fail: 狀態機異常結束;
- SubStateMachine: 調用子狀態機;
- ServiceName: 服務名稱,通常是服務的beanId;
- ServiceMethod: 服務方法名稱;
- CompensateState: 該"狀態"的補償"狀態";
- Input: 調用服務的輸入參數列表,是一個數組,對應於服務方法的參數列表, $.表示使用表達式從狀態機上下文中取參數,表達使用的 SpringEL[8], 如果是常量直接寫值即可;
- Output: 將服務返回的參數賦值到狀態機上下文中,是一個 map 結構,key 為放入到狀態機上文時的 key(狀態機上下文也是一個 map),value 中 $. 是表示 SpringEL 表達式,表示從服務的返回參數中取值,#root 表示服務的整個返回參數;
- Status: 服務執行狀態映射,框架定義了三個狀態,SU 成功、FA 失敗、UN 未知,我們需要把服務執行的狀態映射成這三個狀態,幫助框架判斷整個事務的一致性,是一個 map 結構,key 是條件表達式,一般是取服務的返回值或拋出的異常進行判斷,默認是 SpringEL 表達式判斷服務返回參數,帶 $Exception{開頭表示判斷異常類型,value 是當這個條件表達式成立時則將服務執行狀態映射成這個值;
- Catch: 捕獲到異常後的路由;
- Next: 服務執行完成後下一個執行的"狀態";
- Choices: Choice 類型的"狀態"里, 可選的分支列表, 分支中的 Expression 為 SpringEL 表達式,Next 為當表達式成立時執行的下一個"狀態";
- ErrorCode: Fail 類型"狀態"的錯誤碼;
- Message: Fail 類型"狀態"的錯誤資訊;
更多詳細的狀態語言解釋請看《Seata Saga 官網文檔》[6]。
狀態機引擎原理

- 圖中的狀態圖是先執行 stateA, 再執行 stataB,然後執行 stateC;
- "狀態"的執行是基於事件驅動的模型,stataA 執行完成後,會產生路由消息放入 EventQueue,事件消費端從 EventQueue 取出消息,執行 stateB;
- 在整個狀態機啟動時會調用 Seata Server 開啟分散式事務,並生產 xid, 然後記錄"狀態機實例"啟動事件到本地資料庫;
- 當執行到一個"狀態"時會調用 Seata Server 註冊分支事務,並生產 branchId, 然後記錄"狀態實例"開始執行事件到本地資料庫;
- 當一個"狀態"執行完成後會記錄"狀態實例"執行結束事件到本地資料庫, 然後調用 Seata Server 上報分支事務的狀態;
- 當整個狀態機執行完成,會記錄"狀態機實例"執行完成事件到本地資料庫, 然後調用 Seata Server 提交或回滾分散式事務;
狀態機引擎設計

狀態機引擎的設計主要分成三層, 上層依賴下層,從下往上分別是:
- Eventing 層:
- 實現事件驅動架構, 可以壓入事件, 並由消費端消費事件, 本層不關心事件是什麼消費端執行什麼,由上層實現;
- ProcessController 層:
- 由於上層的 Eventing 驅動一個「空」流程執行的執行,"state"的行為和路由都未實現,由上層實現;
基於以上兩層理論上可以自定義擴展任何"流程"引擎。這兩層的設計是參考了內部金融網路平台的設計。
- StateMachineEngine 層:
- 實現狀態機引擎每種 state 的行為和路由邏輯;
- 提供 API、狀態機語言倉庫;
Saga 模式下服務設計的實踐經驗
下面是實踐中總結的在 Saga 模式下微服務設計的一些經驗,當然這是推薦做法,並不是說一定要 100% 遵循,沒有遵循也有「繞過」方案。
好消息:Seata Saga 模式對微服務的介面參數沒有任務要求,這使得 Saga 模式可用於集成遺留系統或外部機構的服務。
允許空補償
- 空補償:原服務未執行,補償服務執行了;
- 出現原因:
- 原服務 超時(丟包);
- Saga 事務觸發回滾;
- 未收到原服務請求,先收到補償請求;
所以服務設計時需要允許空補償,即沒有找到要補償的業務主鍵時返回補償成功並將原業務主鍵記錄下來。
防懸掛控制
- 懸掛:補償服務 比 原服務 先執行;
- 出現原因:
- 原服務 超時(擁堵);
- Saga 事務回滾,觸發回滾;
- 擁堵的原服務到達;
所以要檢查當前業務主鍵是否已經在空補償記錄下來的業務主鍵中存在,如果存在則要拒絕服務的執行。
冪等控制
- 原服務與補償服務都需要保證冪等性, 由於網路可能超時,可以設置重試策略,重試發生時要通過冪等控制避免業務數據重複更新。
總結
很多時候我們不需要強調強一性,我們基於 BASE 和 Saga 理論去設計更有彈性的系統,在分散式架構下獲得更好的性能和容錯能力。分散式架構沒有銀彈,只有適合特定場景的方案,事實上 Seata Saga 是一個具備「服務編排」和「Saga 分散式事務」能力的產品,總結下來它的適用場景是:
- 適用於微服務架構下的「長事務」處理;
- 適用於微服務架構下的「服務編排」需求;
- 適用於金融核心系統以上的有大量組合服務的業務系統(比如在渠道層、產品層、集成層的系統);
- 適用於業務流程中需要集成遺留系統或外部機構提供的服務的場景(這些服務不可變不能對其提出改造要求)。
文中涉及相關鏈接
[1]https://github.com/aphyr/dist-sagas/blob/master/sagas.pdf
[2]http://microservices.io/patterns/data/saga.html
[3]Microprofile 的 LRA:
https://github.com/eclipse/microprofile-sandbox/tree/master/proposals/0009-LRA
[4]Eventuate Tram Saga:
https://github.com/eventuate-tram/eventuate-tram-sagas
[5]ServiceComb Saga:
https://github.com/apache/servicecomb-pack
[6]Seata Saga 官網文檔:
http://seata.io/zh-cn/docs/user/saga.html
[7]AWS Step Functions:
https://docs.aws.amazon.com/zh_cn/step-functions/latest/dg/tutorial-creating-lambda-state-machine.html
[8]SpringEL:
https://docs.spring.io/spring/docs/4.3.10.RELEASE/spring-framework-reference/html/expressions.html