Dev Study
AWSサービスの内部原理 コース

94. Sagaは打ち消しで巻き戻す分散トランザクションである — 補償の連鎖をCatchで宣言する

単一DBならロールバック1命令で消える「なかったこと」を、複数のマイクロサービスを跨ぐと自分で逆再生の物語として書かねばならない——その巻き戻しをStep FunctionsのCatchでどう宣言するかを、図で追っていきます。

① なぜ分散ではACIDのロールバックが効かないのか

注文→決済→在庫引当一連の更新処理
しかし現実は…
ACIDトランザクションBEGIN…全部やる…COMMIT
途中で失敗してもROLLBACK 1命令で全消し。ロックが「中途半端な状態」を隠す
1つのCOMMITで束ねられない
Order-Service自分のDB-Aを持つ
Payment-Service自分のDB-Bを持つ
Inventory-Service自分のDB-Cを持つ
ロックで守れない「全部やるか全部やらないか」を保証できない
  • 単一DBのACIDは、コミット前の中間状態をロック/分離で外から見えなくし、失敗時はROLLBACKという1命令で巻き戻す。サービスごとにDBが分かれると、この「1つのコミット境界」が消える。
  • DynamoDB編で見た結果整合性の裏返しで、分散では「全員が一致した瞬間」を作ること自体が高くつく——だからその瞬間を強制するロックという発想を捨てる必要がある。

複数のマイクロサービスがそれぞれ自分のDBを持つと、単一DBのACIDのように「全部やるか全部やらないか」をロックで守れない。ここがSagaが解こうとする問題の出発点。

② Sagaの答え — ローカルトランザクションの連鎖と逆順の打ち消し

T1: 注文作成DB-Aにローカルコミット
T2: 決済DB-Bにローカルコミット
T3: 在庫引当ここで失敗
T3自身はコミットされていないので打ち消し不要
次の補償へ
C2: 決済取消T2の打ち消し
巻き戻し完了
C1: 注文キャンセルT1の打ち消し
失敗として終了中途半端な状態を残さない
  • 公式定義(AWS Prescriptive Guidance)——「The saga pattern is a failure management pattern that helps establish consistency in distributed applications」。
  • 各トランザクションごとにイベントを発行し、「the next transaction is initiated based on the event's outcome. It can take two different paths, depending on the success or failure」——成功と失敗で別の道を辿る。
  • 補償トランザクション(compensating transaction)は成功済みのローカルコミットを意味的に取り消す操作。どこで失敗したかによって巻き戻す範囲(=それまでに成功した分)が変わる。もしT2(決済)で失敗なら、走る補償はC1(注文キャンセル)だけ——公式の図でもProcessPaymentの失敗はCancel Order等の巻き戻しステップへ遷移する。

Sagaは処理をローカルトランザクションの連鎖として進め、途中で失敗したら、成功済みの分だけを逆順に補償トランザクションで巻き戻す失敗管理パターン。成功と失敗で別の道(two different paths)を辿るのが核心。

③ 使いどころと正直な代償 — 3条件と公式の警告

疎結合の整合性密結合なしに複数サービス間の整合性が要る
長寿命Tx他サービスをブロックしたくない
失敗時ロールバック失敗したら巻き戻しが必要
複雑さという代償補償ロジックの設計は複雑なプログラミングモデル
サービス数が増えるほどデバッグの難度も上がる
  • 公式の警告(Important)——「The saga pattern is difficult to debug and its complexity increases with the number of microservices. The pattern requires a complex programming model that develops and designs compensating transactions for rolling back and undoing changes.」ロックを捨てた代わりに、巻き戻しの物語を自分で全部書く責任を負う。

Sagaは「密結合なしの整合性/長寿命処理/失敗時ロールバック」の3条件で効く一方、補償ロジックの設計はサービス数とともに複雑化する——タダの解決策ではないとAWS自身が明記している。

④ Step Functionsでの組み立て — 各TaskにCatchを付けて補償へ分岐させる

CreateOrderTask状態
Catch(States.ALL)最初のステップなので補償不要 → Fail
成功=Next
ProcessPaymentTask状態
Catch→補償1段CancelOrder → Fail
全ステップ成功
ReserveInventoryTask状態
Catch→補償2段RefundPayment → CancelOrder → Fail
ここまで進んでいた分だけ、逆順に打ち消す
完了Succeed状態
  • Catchフィールド(catchers)を持てるのはTask・Parallel・Mapの3状態。各catcherはErrorEquals(捕まえるエラー名の配列)とNext(遷移先。ステート名に厳密一致する文字列)を必須で持つ。
  • States.ALLはエラー名に広く一致するワイルドカードで、ErrorEquals配列内では単独で置き、かつCatch配列の最後のcatcherに置く必要がある——ただしStates.DataLimitExceededとStates.RuntimeだけはStates.ALLでも捕まえられない。
  • RetryとCatchを両方持つ場合、Step Functionsは先にretrierを試し、解決できなければcatcherの遷移を適用する。
  • 図中のステート名(CreateOrder等)は例示であり、公式サンプルの逐語ではない。

各Task状態にCatchを付け、失敗したら「そのステップまでの補償状態」へNextで遷移させる。Task/Parallel/Mapだけがcatcherを持てるので、補償が要る操作はこの3種の状態で表現する。これは前のRetry/Catch回で学んだ分岐機構を、そのまま巻き戻しの経路づくりに使っているだけ。

⑤ 補償は「何を打ち消すか」を知る必要がある — エラー出力をResultPathで運ぶ

ProcessPayment失敗Step Functionsがエラー出力(error output)を生成
CatchのResultPathで「入力のどこに」置くか決める
{Error, Cause}Causeに人間可読のエラー説明
追記して渡すと
省略 = "$"入力全体をエラー出力で上書き
元の入力(注文ID等)が消える
"$.error-info"元の入力を残したままエラーを追記
CancelOrder(補償)orderId(元の入力)+error-info(失敗の原因)が両方揃う
「どの注文を、なぜ打ち消すのか」を補償側が知れる
  • エラー出力のオブジェクトは通常Causeフィールド(人間可読の説明)を含む。
  • catcherのResultPathは「catcherがNext先へ何を入力として送るか」を決めるJSONPath。指定するとエラー出力を入力に追記できる(公式例: "ResultPath": "$.error-info")。
  • JSONPathワークフローで省略するとデフォルトは$となり、入力全体をエラー出力で上書きする。

ResultPathでエラー出力(Cause)を元の入力に追記して運べば、補償トランザクションは「何を打ち消すべきか」を知れる。ResultPathを省略するとエラー出力が入力を全部上書きしてしまい、打ち消し対象のID等が消える——だから補償を跨ぐSagaでは追記する形が定石。

⑥ 補償が事故らないための土台 — exactly-onceと冪等性

Standard実行exactly-once: タスク/状態は二度実行されない
ASLでRetryを明示した時を除く
補償を冪等に何度呼んでも結果が同じになるよう作る
再試行に備える
締め
ちょうど1回の効果補償の二重実行事故を減らし、万一の二重でも壊れない
逆再生の物語DBの中ではロールバックは1命令、分散では自分で書く
  • Standard Workflowsは「exactly-once model, where your tasks and states are never run more than once, unless you have specified Retry behavior」(ASLでRetryを指定した場合を除く)。公式はこのexactly-onceモデルゆえに、決済処理のような非冪等(non-idempotent)な操作の編成にStandardが向くと明記している。
  • Retryを明示すれば同じ操作が再実行され得るので、補償操作(RefundPayment等)は再試行に備えて冪等に作る——メッセージング編のat-least-once下の冪等性の実戦投入そのもの。
  • Express(Async)はat-least-once、Express(Sync)はat-most-once。確実に最後まで走り切る耐久実行の土台(イベントログ回)が、この巻き戻しを途中で取りこぼさない前提になっている。

exactly-once実行(Standard)が「補償が二重に走る」事故を減らし、補償操作自体を冪等に作ることで再試行にも耐える。ロックで守る(悲観)のではなく失敗したら打ち消す(楽観)——DBの中ではロールバックは1命令、分散では自分で書く逆再生の物語である。

公式ドキュメントで詳しく ↗