如果你是跟著文章一天接著一天實作的讀者,那麼你需要確保你的 :
Anser-Tutorial-Service
使用的是最新的v1.0.5
以上的版本User Service
使用的是最新的v1.1.2
以上的版本Production Service
使用的是最新的v1.0.3
以上的版本Order Service
使用的是最新的v1.1.2
以上的版本
當一個交易涉及多個服務時,我們需要一個可靠的機制來確保所有的步驟都成功執行,或在遇到失敗時能夠恢復到原先的狀態。而 Anser-Saga 為此而設計,它是一個基於Anser-Orchestration 元件的協作器交易設計,主要負責在分散式交易中管理和執行每個步驟的補償方法。
在本章的範例程式碼中,我們將展示如何利用 Anser-Saga 來設計和實現一個用於建立訂單的協作器。在這個例子中,我們會看到如何定義各個交易步驟,以及如何為每個步驟設定補償策略來保證資料的一致性和交易的完整性。
建立訂單的協作器與可能的錯誤
我們重新再拿出第十六章的協作器設計圖,會發現除了第一與第二步驟外,剩下的步驟都涉及資料的敏感操作。協作器是依步驟順序執行的,也就是說從商品庫存扣除開始,只要有任何一個步驟的行動出現錯誤,那麼整個協作器將會停擺,已經執行的更動將會造成資料不一致的結果。
為了要解決上述問題,在 Anser-Saga 元件中,你能夠獨立撰寫每個步驟的補償方法。請在專案中建立起 {Anser-Tutorial-Service}/Orchestrators/Sagas/CreateOrderSaga.php
:
<?php
namespace Orchestrators\Sagas;
use SDPMlab\Anser\Orchestration\Saga\SimpleSaga;
use SDPMlab\Anser\Service\ConcurrentAction;
use Orchestrators\CreateOrderOrchestrator;
class CreateOrderSaga extends SimpleSaga
{
public function rollbackInventory()
{
/** @var CreateOrderOrchestrator */
$runTimeOrchestrator = $this->getOrchestrator();
$orderProducts = $runTimeOrchestrator->orderProducts;
$concurrentAction = new ConcurrentAction();
foreach ($orderProducts as $product) {
$concurrentAction->addAction(
'rollbackInventory_' . $product->p_key,
$runTimeOrchestrator->productionService->addInventoryCompensateAction($product->p_key, $runTimeOrchestrator->orderId, $product->amount)
);
}
$concurrentAction->send();
}
public function rollbackOrder()
{
/** @var CreateOrderOrchestrator */
$runTimeOrchestrator = $this->getOrchestrator();
$userKey = $runTimeOrchestrator->getStepAction('userInfo')->getMeaningData()['data']['u_key'];
$runTimeOrchestrator->orderService->compensateOrderAction($userKey, $runTimeOrchestrator->orderId)->do();
}
public function rollbackUserWalletCharge()
{
/** @var CreateOrderOrchestrator */
$runTimeOrchestrator = $this->getOrchestrator();
$userKey = $runTimeOrchestrator->getStepAction('userInfo')->getMeaningData()['data']['u_key'];
$total = $runTimeOrchestrator->getStepAction('createOrder')->getMeaningData()['total'];
$runTimeOrchestrator->userService->walletCompensateAction($userKey, $runTimeOrchestrator->orderId, $total)->do();
}
}
CreateOrderSaga
類別繼承了 SimpleSaga
,這意味著我們可以在這個協作器中以 $this->getOrchestrator()
取得在執行週期內的協作器實體。你可以關注一下這個寫法:
/** @var CreateOrderOrchestrator */
$runTimeOrchestrator = $this->getOrchestrator();
雖然 getOrchestrator()
會回傳符合 OrchestratorInterface
介面的協作器實體,但由我們自己撰寫的協作器有著許多與業務邏輯圍繞的成員變數、方法,以及服務抽象化後的 Service
類別。因此,我們可以透過 /** @var CreateOrderOrchestrator */
來提醒 IDE 這個變數並非單純的 OrchestratorInterface
,而是 CreateOrderOrchestrator
類別的實體。這將幫助我們在撰寫上能夠享有程式碼自動完成、提示,甚至是通過靜態語法檢查。
rollbackInventory()
、rollbackOrder()
,以及 rollbackUserWalletCharge()
是為不同協作器步驟所定義的補償方法,它們都是公開方法並且只能是公開方法。同時,它們分別負責恢復庫存、取消訂單和退還使用者錢包餘額。
rollbackInventory()
orderProducts
陣列,為每個產品建立一個新的行動,並將它們添加到 ConcurrentAction
實體中,最後以並行的方式進行大規模的補償。rollbackOrder()
CreateOrderOrchestrator
實體中取得已經完成的 Action
並拿到使用者的 key 和訂單 ID。使用這些資訊,我們就能夠呼叫 compensateOrderAction
方法來還原已建立的訂單。rollbackUserWalletCharge()
getOrchestrator
方法取得當前的 CreateOrderOrchestrator
實體,並從中取得 key、訂單 ID 和訂單總金額,最後將它們用於使用者錢包補償。在 Anser 協作器設計模式中,允許你在合適的地方加上交易的保護,你可以依照下列範例的參考改寫你的 {project_root}/Orchestrators/CreateOrderOrchestrator.php
:
<?php
namespace Orchestrators;
use SDPMlab\Anser\Orchestration\Orchestrator;
use Services\OrderService;
use Services\ProductionService;
use Services\UserService;
use Services\Models\OrderProductDetail;
use Orchestrators\Sagas\CreateOrderSaga;
class CreateOrderOrchestrator extends Orchestrator
{
public UserService $userService;
public OrderService $orderService;
public ProductionService $productionService;
protected string $authToken;
/**
* @var OrderProductDetail[]
*/
public array $orderProducts;
public ?string $orderId = null;
/**
* CreateOrderOrchestrator
*
* @param string $authToken
* @param OrderProductDetail[] $orderProducts
*/
public function __construct(string $authToken, array $orderProducts)
{
$this->authToken = $authToken;
$this->orderProducts = $orderProducts;
$this->userService = new UserService();
$this->orderService = new OrderService();
$this->productionService = new ProductionService();
$this->orderId = $this->generateOrderId();
}
/**
* definition of orchestrator
*
* @return void
*/
protected function definition()
{
//Step0 取得使用者資訊(驗證使用者)
$this->setStep()->addAction(
alias: 'userInfo',
action: $this->userService->userInfoAction($this->authToken)
);
//Step1 取得產品最新資訊(用於取得最新售價)
$step1 = $this->setStep();
foreach ($this->orderProducts as $index => $orderProduct) {
$step1->addAction(
alias: 'product_' . ($orderProduct->p_key),
action: $this->productionService->productInfoAction($orderProduct->p_key)
);
}
$this->transStart(transactionClass: CreateOrderSaga::class);
//Step2 扣商品庫存
$step2= $this->setStep()->setCompensationMethod('rollbackInventory');
foreach ($this->orderProducts as $index => $orderProduct) {
$step2->addAction(
alias: 'product_' . ($orderProduct->p_key) . '_reduceInventory',
action: $this->productionService->reduceInventory($orderProduct->p_key, $this->orderId, $orderProduct->amount)
);
}
//Step3 建立訂單(將會取得訂單總價 total)
$this->setStep()->setCompensationMethod('rollbackOrder')
->addAction(
alias: 'createOrder',
action: static function (CreateOrderOrchestrator $runtimeOrch) {
$userKey = $runtimeOrch->getStepAction('userInfo')->getMeaningData()['data']['u_key'];
//將最新商品售價更新至訂單資訊
foreach ($runtimeOrch->orderProducts as &$product ) {
$product->price = (int)$runtimeOrch->getStepAction('product_' . $product->p_key)->getMeaningData()['data']['price'];
}
return $runtimeOrch->orderService->createOrderAction($userKey, $runtimeOrch->orderId, $runtimeOrch->orderProducts);
}
);
//Step4 使用者錢包扣款
$this->setStep()->setCompensationMethod('rollbackUserWalletCharge')
->addAction(
alias: 'walletCharge',
action: static function (CreateOrderOrchestrator $runtimeOrch) {
$userKey = $runtimeOrch->getStepAction('userInfo')->getMeaningData()['data']['u_key'];
$total = $runtimeOrch->getStepAction('createOrder')->getMeaningData()['total'];
return $runtimeOrch->userService->walletChargeAction($userKey, $runtimeOrch->orderId, $total);
}
);
$this->transEnd();
}
protected function defineResult(): array
{
$orderInfo = $this->getStepAction('createOrder')->getMeaningData();
$data = [
"success" => true,
"message" => "訂單建立成功",
"data" => [
"order_id" => $this->orderId,
"total" => $orderInfo['total'],
]
];
return $data;
}
protected function defineFailResult(): array
{
$failActions = $this->getFailActions();
$failMessages = [];
foreach ($failActions as $failAction) {
$failMessages[] = $failAction->getMeaningData();
}
$data = [
"success" => false,
"message" => "訂單建立失敗",
"data" => [
"order_id" => $this->orderId,
"fail_messages" => $failMessages,
]
];
return $data;
}
protected function generateOrderId(): string
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
random_int(0, 0xffff),
random_int(0, 0xffff),
random_int(0, 0xffff),
random_int(0, 0x0fff) | 0x4000,
random_int(0, 0x3fff) | 0x8000,
random_int(0, 0xffff),
random_int(0, 0xffff),
random_int(0, 0xffff)
);
}
}
$this->transStart(string $className)
是 Saga 模式的開始,你需要在這個方法中傳入 Class Name。在這個狀況下,transStart
表示開始一個需要多個步驟,並且可能需要還原的過程。如果在這個過程中的任何一步失敗,系統會執行相應的補償策略來還原先前的操作。rollbackInventory
來處理可能的失敗。$this->setStep()->setCompensationMethod(string $methodName)
其中的 $methodName
指的就是 transStart
類別中的公開方法的名稱。rollbackOrder
,以處理可能的失敗。rollbackUserWalletCharge
,以處理可能的失敗。$this->transEnd()
通知協作器交易已結束。透拓 transStart
和 transEnd
方法,可以明確地標記出需要保證一致性的操作範圍。如果在這個範圍內的任何操作失敗,Saga 模式會觸發相應的補償方法來還原先前的操作,以保證系統的一致性。
從 Step2
開始前才呼叫 $this->transStart
是因為,只有在進行庫存扣減、訂單建立和使用者錢包扣款等操作時,才需要考慮可能的失敗和補償。而在此之前的步驟,例如取得使用者資訊和產品資訊,不涉及到可能需要回滾的操作。
你可以將建立商品的 Raw json 其中一個商品寫入一個極大的數字,這將觸發商品的庫存不足以扣除而進行還原:
[POST]
{{main_service}}/create_order.php
[ { "p_key": 1, "price": 450, "amount": 5 }, { "p_key": 2, "price": 70, "amount": 4 }, { "p_key": 55, "price": 70, "amount": 400000 } ]
此時,前往 Production Service 的資料庫,你會看到除了無法建立的 "p_key": 55
商品以外,其餘的商品都經歷了建立並且還原的過程。
接著,你可以登入錢包餘額不足的使用者帳號,比如說:{ "email" : "user2@anser.io", //使用者帳號 "password" : "password" //使用者密碼 }
取得 Token 後進行一個正常的建立流程,並觀察整體的還原狀態:
[POST]
{{main_service}}/create_order.php
[ { "p_key": 1, "price": 450, "amount": 5 }, { "p_key": 2, "price": 70, "amount": 4 } ]
此時,你可以到 Order Service 看到訂單被建立後又被 deleted_at
軟刪除。這意味著這筆訂單將會永遠消失於使用者的視野。
同時,你也可以看到好不容易被扣庫存成功的兩個商品又被補償了庫存。
在本章中,我們學習了如何依據需要撰寫正確的 Saga
補償類別,並且也學會了如何與現有的協作器進行整合,使你能夠替你的協作器輕鬆地加上補償機制。隨著對 Saga 設計模式的理解加深,我們將能夠更有效地處理微服務架構中的長期事務和一致性問題。