領域驅動模型DDD(三)——使用Saga管理事務

阿波羅的手發表於2022-04-22

前言

雖然一直說想寫一篇關於Saga模式,在多次嘗試後不得不承認這玩意兒的仿製程式碼真不是我一個菜鳥就能完成的,所以還是妥協般地引用現成的Eventuate Tram Saga框架(雖然我對它一直很反感)和Seata的Saga模式。有一說一,我極其不願意採用這種封裝好的框架和解決方案對知識進行講解,因為龐大的架構和原始碼對讀者來說跨度太大,可是如果想把它內部流程講清又要花費很大的精力進行詳解,而且太考驗文章的敘述文字功底了。

這有違我一直提倡的:“架構”是一種理論思想而非具體百搭的程式碼段。鑑於此,此章我會竭盡所能把基於框架的Saga模式講解清晰,希望讀者能夠反覆閱讀,有不解的地方盡情在評論區詢問。

Saga是什麼?

分散式事務的挑戰

在分散式系統中會把一個應用系統拆分為可獨立部署的多個服務,因此需要服務與服務之間遠端協作才能完成事務操作,這種分散式系統環境下的保證事務特性(ACID)的機制運轉稱之為分散式事務。

多個服務、資料庫、訊息代理之間在很多時候是必須維持資料一致性。特別是涉及金融交易的模組,即使可能只是出現1的偏差,也會因為蝴蝶效應而導致巨大損失。最原始的分散式事務管理的實施標準是XA模式(Seata AT和XA模式),其採用了兩段式提交來保證事務中所有參與者都是健康且同時完成提交,或則在事務失敗時同時進行回滾。

但XA模式也衍生出相關的問題:1、許多新技術包括NoSQL、RabbitMq並不支援XA標準的分散式事務;2、其效能上因為採用阻塞性協議,所有參與節點都是事務阻塞型的。當參與者佔有公共資源時,其他第三方節點訪問公共資源不得不處於阻塞狀態,這就容易出現響應時間增長、死鎖等問題;3、由於分散式事務採用的時同步程式間通訊(傳送請求就必須收到應答,否則不進行下次傳輸),這導致一旦分散式事務中的一個參與方出現問題就會讓整個系統無法正常使用,降低了系統總體的可用性。

Eric Brewer在著名的CAP理論中證明了系統只要在一致性、可用性、分割槽容錯性中保證兩個就行。

分割槽容錯性:在分散式系統中因為網路通訊故障而導致整個整體被分割成一個個分割槽,而這些分割槽仍可以繼續對外供滿足一致性和可用性的服務。(必要的)

可用性:系統一直處於可用狀態,能正常響應資料,但是不保證響應資料為最新資料。

一致性:資料在多個副本之間能夠保持一致的特性(Mysql主從表)。

今天,大部分架構師會傾向保證系統可用性,而將資料強制一致性的要求降低至滿足最終一致便可。Saga正是為了解決微服務架構下資料一致性的問題,構建在鬆耦合、非同步服務之上的機制。

Saga的機制

當本地事務完成時服務就會發布訊息(圖中Create order完成時釋出訊息),然後觸發Saga中的下一個步驟(觸發庫存服務中預減庫存)。通過訊息傳送的方式可以確保所有參與服務之間的鬆耦合,也可以讓整個業務流程實現非同步通訊,即使其中一些訊息的接收方不可用,訊息代理也會先進性快取,直到訊息被接受為止。

Saga的補償式事務回滾

用八個字即可概括:有則繼續,無則補償。例如:當我從錢包中拿出5塊錢跟櫃員說從冰箱裡拿一根巧克力甜筒,如果櫃員發現冰箱中有巧克力甜筒,就把甜筒拿出給我,把錢放入收銀櫃中。如果櫃員發現冰箱沒有巧克力甜筒,則把錢返回給我放回錢包

以上例子中從 ①我拿出錢支付->②櫃員接過錢確認有存貨->③給我甜筒->④將錢放入收銀櫃中->⑤我離開店鋪 是一整個活動流程。其中每一小段都是一個事務(不可分割),當沒有存貨時就把錢返還給我確保錢包的錢沒有少,這裡的還錢便是給錢的補償事務。

其中①-③被稱為可補償性事務,因為它們後面跟著的步驟可能失敗,例如對①來說失敗情況就是沒有存貨,那麼補償事務就是還錢;對③來說失敗情況可能是給了假鈔,那麼補償事務就是要回甜筒放回貨櫃裡。第④步被稱為關鍵性事務,因為後面跟著的是不可能失敗的步驟(如果第④步已經成功完成,其實已經說明整個流程是成功的,因為我肯定能離開店鋪)。對於最後的步驟成為可重複性事務,因為它們一定會成功。

Saga的協調模式

·協同式:把Saga的決策和決定執行順序的邏輯分佈在Saga的每一個參與方上,讓它們各自通過交換事件的方式進行溝通。(各管各的)

·編排器:把Saga的決策和決定執行順序的邏輯集中在一個Saga編排器類(理解成一個管理中心)上,Saga編排器發出命令式的訊息給每一個參與方,指揮這些參與方完成具體操作。(中央排程)

協同式Saga

在下圖中是簡單的訂單建立->支付成功->完成訂單的流程,其中庫存服務和支付服務通過訂閱相關的訊息通道接受事件訊息,從而能夠按順序完成需要自己參與的事務工作。

以上是支付成功情況下,支付服務就釋出PaySucceed事件,之後庫存服務和訂單服務就陸續完成減庫存和完成訂單的操作。

而出現支付失敗情況下,支付服務就釋出PayFail事件,庫存服務會消費該事件並進行Inventory cancelReduction(減庫存取消)併發布ReductionFail(減庫存失敗),訂單服務就會消費該事件並進行Cancel order(取消訂單)操作。

協同式的好處:

·鬆耦合:一眼看去就知道啦,參與方與訂閱方除了訊息事件的通訊,並不會產生耦合;

·簡單:只需要在建立、更新、刪除業務物件時釋出事件即可;
弊端:

·過度分散:程式碼中沒有集中定義Saga的地方,因為分佈在各個服務中,所以在進行編寫和理解時難度較大;

·服務之間迴圈依賴:參考上圖,參與方和訂閱方身份一直在相互轉換相互訂閱,雖然不一定引發問題,但這種迴圈依賴是一種不好的設計風格;

編排式Saga

編排式時實現Saga的另一種方式,使用此方式時開發人員定義一個編排器類,這個類職責就是對外告訴Saga的參與方需要做什麼。Saga編排器採用同步/非同步方式相應方式進行通訊。

上圖很明顯與協同式Saga相比,流程更加簡潔明瞭。當然它僅僅只是描述了正常情況下整個執行流程,而涉及到失敗情況需要進行實物補償時我們就得藉助狀態機模型(有點類似流程圖,成功時怎麼樣,失敗了怎麼樣)對所有可能出現的場景進行判斷描述。

上圖就是狀態機,例如當狀態機從Inventory reduction(預鎖庫存)狀態可以轉化為Order Pay或者是Reject Inventory reduction狀態,當它收到正常回復Inventory Reduction(鎖庫存成功),那狀態轉化為Order Pay。而如果收到Inventory Reduction Failed回覆(鎖庫存失敗),則狀態轉為Reject Inventory reduction。(注:狀態、訊息回覆、命令都可以根據自己習慣定義)

出上面例舉以外,阿里推出的分散式事務管理Seata中的Saga模式也是基於狀態機引擎的Saga實現,此處我引用官網的圖片簡單講解下(就按流程圖來理解):

示例狀態圖

圖中CompensateReduceInventory與CompensateReduceBalance是補償式事務,當ReduceInventory在理想情況下到達ReduceBalance後觸發了CompensationTrigger(補償觸發器)時,CompensateReduceInventory與CompensateReduceBalance兩個補償式事務就會分別對ReduceInventory和ReduceBalance進行補償,並最終返回Fail的狀態。

可以看到狀態機的存在是為了讓我們更直觀地看清楚Saga整個執行流程,能夠很好地幫助開發人員進行理解,簡化了業務邏輯讓我們的目光更加註重如何改善隔離性問題。並且編排式的Saga參與者之間的依賴關係更加簡單,不存在以來迴圈的問題。

基於Eventuate Tram框架的Saga模式講解

此小章節為了讀者能夠更好地理解Saga模式在實際落地的應用,本人摘選了《微服務架構設計模式》中的一部分程式碼,並對程式碼進行精簡優化以便保證讀者可以從業務流程帶入到程式碼執行程的連貫性,如果希望瞭解詳細程式碼各位可以去看原著,百度也有免費的電子書。

我們先看一副狀態機:

那麼我們現在建立一個Saga編排器(CreateOrderSaga),用看圖寫作的思維,將整個狀態機轉化為一篇流水賬式的程式碼:

public class CreateOrderSaga implements SimpleSaga<CreateOrderSagaState> {

  private Logger logger = LoggerFactory.getLogger(getClass());

  private SagaDefinition<CreateOrderSagaState> sagaDefinition;

  public CreateOrderSaga(OrderServiceProxy orderService, ConsumerServiceProxy consumerService, KitchenServiceProxy kitchenService,
                         AccountingServiceProxy accountingService) {
    this.sagaDefinition =
             step()
              //補償式事務,makeRejectOrderCommand建立“拒絕訂單命令”,使用rderService.reject中配置的規則傳送和回覆
              .withCompensation(orderService.reject, CreateOrderSagaState::makeRejectOrderCommand)
            .step()
              //這一步呼叫參與者,makeValidateOrderByConsumerCommand建立“根據消費者命令進行驗證訂單”的命令,使用consumerService.validateOrder中配置的規則傳送和回覆
              .invokeParticipant(consumerService.validateOrder, CreateOrderSagaState::makeValidateOrderByConsumerCommand)
            .step()
              //這一步呼叫參與者,makeCreateTicketCommand建立“建立收據”的命令,使用kitchenService.create中配置的規則傳送和回覆
              .invokeParticipant(kitchenService.create, CreateOrderSagaState::makeCreateTicketCommand)
              //接受到上一步成功回覆“CreateTicketReply”的命令後,執行handleCreateTicketReply的命令
              .onReply(CreateTicketReply.class, CreateOrderSagaState::handleCreateTicketReply的命令)
              //補償式事務,makeCancelCreateTicketCommand建立“取消建立票據”的命令,使用kitchenService.cancel中的配置傳送和回覆
              .withCompensation(kitchenService.cancel, CreateOrderSagaState::makeCancelCreateTicketCommand)
            .step()
               //這一步呼叫參與者,makeAuthorizeCommand建立“授權”命令,使用accountingService.authorize中的配置傳送和回覆
              .invokeParticipant(accountingService.authorize, CreateOrderSagaState::makeAuthorizeCommand)
            .step()
               //這一步呼叫參與者,makeConfirmCreateTicketCommand建立“確認建立票”,使用kitchenService.confirmCreate中的配置傳送和回覆
              .invokeParticipant(kitchenService.confirmCreate, CreateOrderSagaState::makeConfirmCreateTicketCommand)
            .step()
               //這一步呼叫參與者,makeApproveOrderCommand建立“批准訂單”的命令,使用orderService.approve中的配置傳送和回覆
              .invokeParticipant(orderService.approve, CreateOrderSagaState::makeApproveOrderCommand)
            .build();
  }

  @Override
  public SagaDefinition<CreateOrderSagaState> getSagaDefinition() {
    return sagaDefinition;
  }
}

OrderServiceProxy介面卡:

public class OrderServiceProxy {

  public final CommandEndpoint<RejectOrderCommand> reject = CommandEndpointBuilder
          //要傳送的“拒絕訂單”的命令
          .forCommand(RejectOrderCommand.class)
          //傳送命令到命令通訊通道:OrderServiceChannels.orderServiceChannel
          .withChannel(OrderServiceChannels.orderServiceChannel)
           //回覆Saga編排器執行成功的應答
          .withReply(Success.class)
          .build();

  public final CommandEndpoint<ApproveOrderCommand> approve = CommandEndpointBuilder
           //執行批准訂單的命令
          .forCommand(ApproveOrderCommand.class)
          .withChannel(OrderServiceChannels.orderServiceChannel)
          .withReply(Success.class)
          .build();
}

KitchenServiceProxy介面卡:

public class KitchenServiceProxy {

  public final CommandEndpoint<CreateTicket> create = CommandEndpointBuilder
           //要傳送的“建立票據”的命令
          .forCommand(CreateTicket.class)
           //傳送命令到命令通訊通道:KitchenServiceChannels.kitchenServiceChannel
          .withChannel(KitchenServiceChannels.kitchenServiceChannel)
           //回覆編排器CreateTicketReply的應答
          .withReply(CreateTicketReply.class)
          .build();

  public final CommandEndpoint<ConfirmCreateTicket> confirmCreate = CommandEndpointBuilder
          .forCommand(ConfirmCreateTicket.class)
          .withChannel(KitchenServiceChannels.kitchenServiceChannel)
          .withReply(Success.class)
          .build();
  public final CommandEndpoint<CancelCreateTicket> cancel = CommandEndpointBuilder
          .forCommand(CancelCreateTicket.class)
          .withChannel(KitchenServiceChannels.kitchenServiceChannel)
          .withReply(Success.class)
          .build();

}

命令傳輸的通訊通道:

public class OrderServiceChannels {
  public static final String orderServiceChannel = "orderService";
}

public class KitchenServiceChannels {
  public static final String kitchenServiceChannel = "kitchenService";
}

OrderCommandHandlers定義了接受到命令訊息後的具體操作:

public class OrderCommandHandlers {

  @Autowired
  private OrderService orderService;

  public CommandHandlers commandHandlers() {
    return SagaCommandHandlersBuilder
          //從此命令通道中獲取命令訊息
          .fromChannel("orderService")
          //接受到批准訂單命令後執行approveOrder方法
          .onMessage(ApproveOrderCommand.class, this::approveOrder)
          //接受到拒絕訂單命令後執行rejectOrder方法,後面都是如此,自己理解
          .onMessage(RejectOrderCommand.class, this::rejectOrder)
          .onMessage(BeginCancelCommand.class, this::beginCancel)
		......
          .build();

  }

  public Message approveOrder(CommandMessage<ApproveOrderCommand> cm) {
    long orderId = cm.getCommand().getOrderId();
    orderService.approveOrder(orderId);
    return withSuccess();
  }


  public Message rejectOrder(CommandMessage<RejectOrderCommand> cm) {
    long orderId = cm.getCommand().getOrderId();
    orderService.rejectOrder(orderId);
    return withSuccess();
  }
.........
}

至此,整個關於Saga基礎內容講解完畢,大家可以根據上面的狀態機和程式碼試著畫一下Saga編排器的設計圖,看看是否真地對Saga內容有進一步的瞭解。

結語

本文內容結構參照了《微服務架構設計模式》,是在對此書閱讀以及結合網路上其他的相關案例總結成連貫,翻譯更準確的知識點,其中因為個人水平問題可能存在歧義或疏漏,希望各位讀者存在疑問或覺得文章中有需要改正的地方盡情指出。之後將在Saga的基礎上講解事件溯源和CQRS,敬請期待。

相關文章