事件消費者之 Saga - 事件溯源

xuding發表於2021-08-02

本文轉載自【何以解耦】:codedecoupled.com/php-es-saga.html

什麼是 Saga

Saga 是一種用於處理漫長業務流程的設計模式。這裡的長度並非時間長短,而是指一個業務流程由於跨域而涉及的領域寬度。所以一個 Saga 處理週期可能是一個星期,一個小時,一分鐘甚至幾秒,它與時間無關。

為什麼使用 Saga

在 DDD(領域驅動)中,我們用聚合建立一個以自我為中心的模型,聚合具有良好的自我保護性,外界只能透過 Command 來呼叫聚合的介面。看起來這是一個很好的設計,然而業務需求層出不窮,當一個業務流程需要多個聚合參與時我們便可使用 Saga。

讓我們舉一個簡單的例子,現有兩個獨立的聚合,他們分別是訂單聚合(Order Aggregate)以及庫存聚合(Inventory Aggregate):

  • Order Aggregate
    • PlaceOrderCommand:觸發 OrderPlacementConfirmedEvent 事件。
  • Inventory Aggregate
    • CheckInventory:觸發 InventoryAvailableEvent 或者 InventoryNotAvailableEvent 事件。

訂單聚合提供兩個對外介面:

  • PlaceOrderCommand:此介面用於提交使用者訂單。

庫存聚合提供一個對外介面:

  • DeductInventory:此介面用於檢查存貨是否足夠。

以上兩個聚合獨立存在且無合作關係,訂單聚合用於提交使用者訂單,庫存聚合用於檢視存貨。此時呼叫 PlaceOrderCommand 並不會檢查存貨,而業務需求肯定會要求提交訂單時確儲存貨足夠,此時訂單聚合與庫存聚合必須相互合作,於是我們便可使用 Saga。

首先我們需要修改訂單聚合介面:

  • Order Aggregate
    • PlacingOrderCommand:觸發 OrderPlacingEvent 事件。
    • ConfirmOrderPlacementCommand:觸發 OrderPlacementConfirmedEvent 事件。

修改後的訂單聚合提供兩個對外介面:

  • PlacingOrderCommand:此介面用於提交使用者訂單。
  • ConfirmOrderPlacementCommand:此介面用於確認使用者訂單的提交。

然後我們便可使用 Saga 來實現業務需求:

class PlaceOrderSaga extends Saga
{
    public function onOrderPlacingEvent(OrderPlacingEvent $event)
    {
        $this
            ->deductInventoryCommand
            ->handle(
                $event->inventoryAggregateId
            );
    }

    public function onInventoryAvailableEvent(InventoryAvailableEvent $event)
    {
        $this
            ->confirmOrderPlacementCommand
            ->handle(
                $event->orderAggregateId
            );
    }
}

我們需要謹記,一個 Saga 是一個業務流程的模型,但是它並不具備任何邏輯程式碼,它僅僅指揮聚合間 API 的呼叫順序。在應用層面,它就像一個簡單的事件監聽器。

我們往往可以用一個簡單的流程圖來梳理 Saga,比如 PlaceOrderSaga:

事件消費者之 Saga - 事件溯源

實現 Saga

以上程式碼僅僅是一種 Saga 的原型圖,在實現 Saga 設計模式時,我們需要注意以下幾點:

排順以及去重

在一個事件驅動系統中,基礎設施的不確定性將導致事件資訊的順序顛倒以及內容重複。比如在使用 AWS SQS 時,如果沒有使用 FIFQ 佇列,訊息的發出順序是不受控的。又比如在 RabbitMQ 中,如果一個訊息沒有被及時消化,同一個訊息可能重發。

基於以上兩點,在實現 Saga 時,它必須同時具備排順以及去重功能,這樣我們的應用層 API 將無後顧之憂。

彌補行為

如果 Saga 在執行過程中發生了異常怎麼辦?比如在我們的例子中,如果最後一步中的 confirmOrderPlacementCommand 由於某種執行失敗,我們應該如何處理?此時的庫存已經扣除,如果不進行處理,庫存一定無法和訂單匹配,這將是一個災難。

在實現 Saga 時,它必須支援彌補行為 ,彌補行為好比資料中的回滾行為,只不過它不是依靠資料庫來實現。

在加入彌補行為後,PlaceOrderSaga 程式碼更新為:

class PlaceOrderSaga extends Saga
{
    public function onOrderPlacingEvent(OrderPlacingEvent $event)
    {
        $this
            ->deductInventoryCommand
            ->handle(
                $event->inventoryAggregateId
            );
    }

    public function onInventoryAvailableEvent(InventoryAvailableEvent $event)
    {
        $this
            ->confirmOrderPlacementCommand
            ->handle(
                $event->orderAggregateId
            );
    }

    public function onInventoryAvailableEventFailed(InventoryAvailableEvent $event)
    {
        $this
            ->increaseInventoryCommand
            ->handle(
                $event->inventoryAggregateId
            );
    }
}

如果 confirmOrderPlacementCommand 失敗,也就是 onInventoryAvailableEvent 失敗,我們在 onInventoryAvailableEventFailed 中將庫存加回去。

注意事項

Saga 是一種容易理解的設計模式,可在一個跨域的場景中,它是一個非常強大的解決方案。最後我們需要注意的,也是上文中未曾提起的一點,那便是如果彌補行為本身失敗了,我們怎麼處理?

如果你的基礎設施能保證彌補行為的穩定性,那是再好不過的了,如果不行的話,我們只能及時的進行人為修復,那便是我們上文中使用的方式。

本文轉載自【何以解耦】: codedecoupled.com/php-es-saga.html ,如果你也對 TDD,DDD 以及簡潔程式碼感興趣,歡迎關注公眾號【何以解耦】,一起探索軟體開發之道。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Know how, know why meanwhile.

相關文章