前言
傳統的分散式事務管理方法對於現代應用程式來說不是一個好的選擇,跨服務的操作必須使用所謂的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事務模型引起的併發異常的設計策略。應用甚至可能需要使用鎖來簡化邏輯,即使這樣會導致死鎖。