本文轉載自【何以解耦】: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 設計模式時,我們需要注意以下幾點:
排順以及去重
在一個事件驅動系統中,基礎設施的不確定性將導致事件資訊的順序顛倒以及內容重複。比如在使用 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 協議》,轉載必須註明作者和本文連結