《微服務架構設計模式》讀書筆記 | 第4章 使用Saga管理事務
- 2021 年 8 月 17 日
- 筆記
- 《微服務架構設計模式》閱讀筆記, 學習筆記
前言
傳統的分佈式事務管理方法對於現代應用程序來說不是一個好的選擇,跨服務的操作必須使用所謂的Saga(一種消息驅動的本地事務序列)來維護數據一致性,而不是ACID事務(原子性、一致性、隔離性和持久性)。
Saga的一個挑戰在於只滿足ACD(原子性、一致性和持久性)特性,而缺乏ACID事務的隔離性。因此應用程序必須使用所謂的對策(countermeasure),找到辦法來防止或減少由於缺少隔離而導致的並發異常
這是一本關於微服務架構設計方面的書,這是本人閱讀的學習筆記。以下對一些符號做些說明:
()為補充,一般是書本里的內容;
[]符號為筆者筆注;
1. 微服務架構下的事務管理
微服務架構下的事務往往需要橫跨多個服務,每個服務都有屬於自己的私有數據庫。在這種情況下,應用程序必須使用一些更為高級的事務管理機制來管理事務。
1.1 分佈式事務的挑戰
在多個服務、數據庫和消息代理之間為此數據一致性的傳統方法是採用分佈式事務。
分佈式事務管理的事實標準是XA標準:
- XA採用兩階段提交來保證事務中所有參與方同時完成提交,或者失敗時同時回滾;
- 應用程序中的整個技術棧需要滿足XA標準(包括數據庫、消息代理、數據庫驅動、消息API等);
其存在的挑戰有:
- 許多新技術,包括NoSQL數據庫(如MongoDB和Cassandra),不支持XA標準的分佈式事務;
- 一些流行的消息代理(如RabbitMQ和Apache Kafka)不支持分佈式事務;
- 分佈式事務本質都是同步進程間通信,會降低分佈式系統的可用性;
1.2 一個Saga的示例
Sage模式:通過使用異步消息來協調一系列本地事務,從而維護多個服務之間的數據一致性。
當本地事務完成時,服務會發佈消息。然後,此消息將觸發Saga中的下一個步驟。
1.3 Saga使用補償事務來回滾所作出的改變
Saga無法自動回滾事務,因為每個步驟都會將其更改提交到本地數據庫。
需要注意不是所有步驟都需要事務補償,如只讀步驟、當某步驟之後的操作總會成功時等。
2. Saga的協調模式
Saga的實現包含協調Saga步驟的邏輯。
2.1 兩種Saga協調模式
- 協同式(choreography):把Saga的決策和執行順序邏輯分佈在Sage的每一個參與方中,它們通過交換事件的方式來進行溝通;
- 編排式(orchestration):【推薦】把Saga的決策和執行順序邏輯集中在一個Saga編排器類中。Saga編排器發出命令式消息給各個Saga參與方,指定這些參與方服務完成具體操作(本地事務);
2.2 實現協同式的Create Order Saga
參與方通過交換事件進行溝通,每個參與方從Order Service開始,更新其數據庫並發佈觸發下一個參與方事件。
- 當第5步失敗時,創建的事件名稱為:
Credit card authorization failed
;
圖解:
- Order Service創建一個處於APPROVAL_PENDING狀態的Order並發佈
OrderCreated
事件; - Consumer Service消費
OrderCreated
事件,驗證消費者是否可以下訂單,並發佈ConsumerVerified
事件; - Kitchen Service消費
OrderCreated
事件,驗證Order,創建一個處於CREATE_PENDING狀態的後廚工單Ticket,並發佈TicketCreated
事件; - Accounting Service消費
OrderCreated
事件並創建一個處於PENDING狀態的CreditCardAuthorization; - Accounting Service消費
TicketCreated
和ConsumerVerified
事件,向消費者的信用卡收費,並發佈CreditCardAuthorized
事件;- 如果失敗,則發佈
CreditCardAuthorizationFailed
事件;
- 如果失敗,則發佈
- Kitchen Service消費
CreditCardAuthorized
事件,將Ticket的狀態更改為AWAITING_ACCEPTANCE;- 如果失敗,則消費
CreditCardAuthorizationFailed
事件,將Ticket的狀態更改為REJECTED;
- 如果失敗,則消費
- Order Service接收
CreditCardAuthorized
事件,將Order的狀態改為APPROVED,並發佈OrderApproved
事件;- 如果失敗,則消費
CreditCardAuthorizationFailed
事件,將Order的狀態更改為REJECTED;
- 如果失敗,則消費
2.3 協同式Sage服務間通信相關的問題
在實現基於協同的Saga時,需要考慮一些與服務間通信相關的問題:
- 確保Saga參與方將更新其本地數據庫和發佈事件作為數據庫事務的一部分;
- 例如:在Create Order Saga中,Kitchen Service接受CreditCarAuthorized事件,創建Ticket,發佈到TicketCreated事件;數據庫的更新和事件發佈必須是原子的;
- 解決:Saga參與方必須使用事務性消息;
- 確保Saga參與方必須能夠將接受到的每個事件映射到自己的數據上;
- 例如:當Order Service收到CreditCardAuthorized事件時,它必須能夠查找相應的Order;
- 解決:讓Saga參與方發佈包含相關性ID的事件;
2.4 協同式Sage的優缺點
好處:
- 簡單:服務在創建、更新和刪除業務對象時發佈事件;
- 松耦合:參與方訂閱事件並且彼此之間不會因此而產生耦合;
弊端:
- 更難理解:協調式Saga的邏輯分佈在每個服務的實現中;開發人員有時很難理解特定Saga是如何工作;
- 服務之間的循環依賴關係:Saga參與方訂閱彼此事件,通常會導致循環依賴關係;
- 緊耦合的風險:每個Saga參與方都需要訂閱所有影響它們的事件;
2.5 實現編排式的Create Order Saga
開發人員定義一個編排器類,該類唯一的職責是告訴Saga的參與方該做什麼事清。Saga編排器使用命令 / 異步響應方式與Saga參與方服務通信。
基於編排式是Saga的每個步驟都包括一個更新數據庫和發佈消息的服務。
圖解:
Order Service首先創建(實例化)一個Order對象和一個Create Order Saga編排器對象,一切正常後流程如下:
- Saga編排器向Consumer Service發送
Verify Consumer
命令; - Consumer Service回復
Consumer Verified
消息; - Saga編排器向Kitchen Service發送
Create Ticket
命令; - Kitchen Service回復
Ticket Created
消息; - Saga編排器向Accounting Service發送
Authorize Card
消息; - Accounting Service使用
Card Authorized
消息回復; - Saga編排器向Kitchen Service發送
Approve Ticket
命令; - Saga編排器向Order Service發送
Approve Ordere
命令(命令式消息);
2.6 把Saga編排器視為一個狀態機
狀態機是由一組狀態和一組由事件觸發的狀態之間的轉換組成。每個轉換都可以有一個動作,對Saga來說動作就是對某個參與方對調用。
將Saga建模成狀態機非常有用,因為它描述了所有可能的場景(可能成功也可能失敗)。
- Verifying Consumer:初始狀態。當處於此狀態時,該Saga正在等待Consumer Service驗證消費者是否可以下訂單;
- Creating Ticket:該Saga正在等待對Create Ticket命令的回復;
- Authorizing Card:等待Authorizing Service授權消費者的信用卡;
- Order Approved:最終狀態,表示該Saga已成功完成;
- Order Rejected:最終狀態,表示Order被其中一個參與方拒絕;
2.7 編排式Saga的優缺點
好處:
- 更簡單的依賴:不會引入循環依賴關係;
- 較少的耦合:每個服務實現供編排器調用的API,因此它不需要知道Saga參與方發佈的事件;
- 改善關注點隔離,簡化業務邏輯:Saga的協調邏輯本地化在Saga編排器中;領域對象更簡單,並且不需要了解它們參與的Saga;
弊端:
- 在編排器中存在集中過多業務邏輯的風險;
- 解決辦法:可以通過設計只負責排序的編排器來避免此問題,並且不包含任何其他業務邏輯;
3. 解決隔離問題
ACID事務的隔離性可確保同時執行多個事務的結果與順序執行它們的結果相同。而Saga只滿足ACD(原子性、一致性、持久性),不滿足隔離性。
3.1 Saga只滿足ACD
- 原子性:Saga實現確保執行所有事務或撤銷所有更改;
- 一致性:服務內的參照完整性(referential integrity)由本地數據庫處理;服務之間的參照完整性由服務處理;
- 持久性:由本地數據庫處理;
3.2 缺乏隔離導致的問題
缺乏隔離將導致以下三個異常。
- 丟失更新:一個Saga沒有讀取更新,而是直接覆蓋了另一個Saga所做的更改;
- 臟讀:一個事務或一個Saga讀取了尚未完成的Saga所做的更新;
- 模糊或不可重複讀:一個Saga的兩個不同步驟讀取相同的數據卻獲得了不同的結果,因為另一個Saga已經進行了更新;
3.3 Saga的結構模型術語
一個Saga包含三個類型的事務。
- 可補償性事務:可以使用補償事務回滾的事務;
- 關鍵性事務:Saga執行過程的關鍵節點。如果關鍵性事務成功,則Saga將一直運行到完成。關鍵性事務不見得是一個可補償性事務,或者可重複性事務。但是它可以是最後一個可補償的事務或第一個可重複的事務;
- 可重複性事務:在關鍵性事務之後的事務,保證成功;
3.4 解決隔離問題的對策
- 語義鎖:應用程序級的鎖;
- Saga的可補償性事務會在其創建或更新的任何記錄中設置標誌,該標誌表示該記錄未提交且可能發生失敗;
- 如:在可補償性事務執行時給操作對象添加上
*_PENDING
狀態,以告訴該對象的其他Saga,該對象當前正處於一個Saga的處理過程中;
- 交換式更新:把更新操作設計成可以按任何順序執行;
- 將更新操作設計為可交換的;
- 如:賬戶的
debit()
和credit()
操作是可交換的;
- 悲觀視圖:重新排序Saga的步驟,以最大限度地降低業務風險;
- 重新排序Saga的步驟,以最大限度地降低由於臟讀而導致的業務風險;
- 重讀值:通過重寫數據來防止臟寫,以在覆蓋數據之前驗證它是否保持不變;
- 使用此對策的Saga在更新之前重新讀取記錄,驗證它是否未更改,然後根性記錄;如果記錄已更改,則Saga將中止並可能重新啟動。此對策是樂觀脫機鎖模式的一種形式;
- 如:可以用來處理Order在批准過程中被取消的情況;
- 版本文件:將更新記錄下來,以便可以對它們重新排序;
- 記錄對數據執行的操作,以便可以對它們進行重新排序;
- 如:當Accounting Service先收到
Cancel Authorization
請求,再收到Authorize Card
請求時,它會注意到它已經收到Cancel Authorization
請求並跳過授權信用卡;
- 業務風險評級(by value):使用每個請求的業務風險來動態選擇並發機制;
- 使用此對策的應用程序使用每個請求的屬性來決定使用Saga和分佈式事務;
4. Order Service和Create Order Saga的設計
示例:使用語義鎖對策的Create Order Saga的詳細設計和實現。
4.1 Order Service的設計及其Saga
- 服務的業務邏輯由傳統的業務邏輯類組成,與傳統業務一樣,業務核心由
OrderService
、Order
和OrderRepository
類實現; - 還有一些Saga編排器類,包括
CreateOrderSaga
類,它可以編排Create Order Saga; - Order Service即是一個Saga編排器,也是一個Saga參與方;Order Service參與它自己的Saga,它有一個
OrderCommandHandlers
適配器類,該適配器類通過調用Order Service來處理命令消息;
4.2 OrderService類
一個由服務的API層調用的領域服務,負責創建和管理訂單。
OrderService的UML類圖:
OrderService類及其createOrder()方法:
4.3 Create Order Saga的實現
使用Eventuate Tram Saga框架編寫,它提供了一種特定於領域的語言(DSL),用於定義Saga的狀態。
OrderService的Saga UML類圖:
- CreateOrderSaga:定義Saga狀態機的單例類;它調用CreateOrderSagaState來創建命令式消息,並使用Saga參與方代理類(如KitchenServiceProxy)指定的消息通道將它們發送給參與方;
- CreateOrderSagaState:一個Saga的持久化狀態,用於創建命令式消息;
- Saga參與方的代理類(如KitchenServiceProxy):每個代理類定義一個Saga參與方的消息API,它由命令通道、命令式消息和回復類型組成。
4.4 CreateOrderSaga編排器
CreateOrderSaga類實現了2.6點的狀態機;它使用Eventuate Tram Saga框架提供的DSL來定義Create Order Saga的步驟;核心代碼為Saga的定義,如下:
4.5 CreateOrderSagaState類
CreateOrderSagaState類表示Saga實例狀態;此類由OrderService創建,並由Eventuate Tram Saga框架持久化保存在數據庫中;其職責為創建發送給Saga參與方的消息;
4.6 KitchenServiceProxy類
代理類不是必要的,使用代理類的好處有兩個:代理類定義靜態類型端點,這減少了Saga向服務發送錯誤消息的可能性;代理類是一個定義良好的調用服務的API,使服務代碼更易於理解和測試;
4.7 Eventuate Tram Saga框架
Eventuate Tram Saga是一個用於編寫Saga編排器和Saga參與方的框架;它使用Eventuate Tram的事務性消息能力。
Eventuate Tram Saga框架:
OrderService創建Create Order Saga實例時的事件序列:
當SagaManager收到來自Saga參與方的回復消息時的事件序列:
4.8 OrderCommandHandlers類
Order Service參與其自己的Saga;OrderCommandHandlers類定義了這些Saga發起命令式消息的處理程序方法。
OrderCommandHandlers UML類圖:
Order Service的命令處理程序:
4.9 OrderServiceConfiguration類
Order Service使用Spring框架;OrderServiceConfiguration是一個@Configuration類,使用Spring @Bean實例化並組裝在一起。
5. 本章小結
- 某些系統操作需要更新分散在多個服務中的數據。傳統的基於XA / 2PC的分佈式事務不適合現代應用。更好的方法是使用Saga模式。Saga是使用消息機制協調的一組本地事務序列。每個本地事務都在單個服務中更新數據。由於每個本地事務都會提交更改,因此如果由於違反業務規則而導致Saga必須回滾,則必須執行補償事務以顯式撤銷更改;
- 可以使用協同或編排協調Saga的步驟。在基於協同的Saga中,本地事務發佈觸發其他參與方執行本地事務的事件。在基於編排的Saga中,集中式Saga編排器向參與方發送命令式消息,告訴它們執行本地事務。可以通過將Saga編排器建模為狀態機來簡化開發和測試,簡單的Saga可以使用協同式,但編排式通常是複雜Saga的更好選擇;
- 設計基於Saga的業務邏輯可能具有挑戰性,因為與ACID事務不同,Saga不是彼此孤立的。你必須經常使用各種對策,即防止ACD事務模型引起的並發異常的設計策略。應用甚至可能需要使用鎖來簡化邏輯,即使這樣會導致死鎖。
最後
