1. 問題
ユーザーが注文を送信した後、在庫、支払い、配送、クーポンの各サービスがフルフィルメントを完了させる必要があります。ビジネスルールやネットワーク障害によってどのステップも失敗する可能性があり、コミットされたリモートトランザクションをロールバックすることはできません。CONFIRMEDまたはCANCELLEDで終了するSagaを設計し、各ローカルステップのトランザクションと補償トランザクションについて説明してください。
2. 制約と明確化事項
- 各サービスは、自身のデータベースに対してのみローカルなACIDトランザクションを実行します。
- 結果整合性は許容されますが、在庫や支払いの保留(ホールド)を無期限に残すことはできません。
- リトライにより1つのステップが複数回実行される可能性があるため、サービスは冪等性キー(idempotency key)を使用して状態を保護する必要があります。
- リトライ可能な技術的障害、リトライ不可能なビジネス上の拒絶、および人手による対応が必要な補償処理の失敗を明確に区別してください。
3. コアアプローチ
Sagaは、長期にわたるトランザクションを順序付けられたローカルトランザクション T1 ... Tn に分解します。成功した各ステップは進行状況を記録して次のステップをトリガーします。後続のステップが失敗した場合、完了済みのステップは逆順で補償トランザクション Ck ... C1 を実行します。補償処理はデータベースのロールバックではなく、在庫の解放、支払いオーソリの取り消し、配送のキャンセル、クーポンの返却など、新たなビジネスオペレーションとなります。
オーケストレーション型のSagaには、状態と次のアクションを保存する永続的なコーディネーターが存在し、明示的なワークフロー、タイムアウト、人的介入の要件に適しています。イベント駆動型のコレオグラフィ(振る舞い)モデルは中央のコーディネーターを排除しますが、可視性やサイクルの制御が難しくなります。どちらのスタイルでも、すべてのコマンドとイベントに saga_id、step_id、バージョン、および冪等性キーを含める必要があります。
4. 参考実装
start(order):
saga = create_saga(order.id, state="RESERVE_STOCK")
dispatch(saga, "ReserveStock")
on_step_result(saga_id, step_id, result):
saga = load_and_lock(saga_id)
require result.version == saga.version + 1
if result.success:
saga.completed_steps.append(step_id)
saga.version += 1
next = next_step(saga)
persist(saga)
dispatch(next) if next else finish_confirmed(saga)
else if result.business_rejection:
saga.state = "COMPENSATING"
persist(saga)
dispatch(compensation_for_last_completed(saga))
else:
schedule_retry_or_timeout(saga, step_id)
on_compensation_result(saga_id, step_id, result):
record_attempt(saga_id, step_id, result)
if result.success:
dispatch(previous_compensation(saga))
else:
mark_manual_intervention(saga, reason=result.error)5. 整合性と正確性
コーディネーターの状態は永続化されている必要があり、そうでない場合はクラッシュによって次のアクションが失われる可能性があります。すべてのコマンドとイベントは冪等性キーを使用します。コンシューマーは、ビジネス上の変更をコミットする前に処理済みの step_id を記録することで、リトライによる在庫の二重確保を防ぎます。また、ステップの完了と次のコマンドの発行の間には送信のギャップが存在するため、結果整合性のある可視性を確保するにはアウトボックスパターン、信頼性の高いキュー、またはCDC(変更データキャプチャ)が必要です。
補償処理は通常、成功した順序の逆順で実行されますが、すべてのアクションに厳密な逆処理が存在するわけではありません。すでに送信された通知を取り消す代わりに返金を行うなど、一部の影響にはビジネス上の調整が必要です。ステータス照会では、「処理中」を成功として報告するのではなく、現在のステップ、完了したステップ、リトライ回数、人的介入の理由を公開する必要があります。
6. フォローアップと落とし穴
- Sagaをデータベース間のアトミックなロールバックとして説明しないでください。Sagaが提供するのは回復可能な結果整合性です。
- 補償処理自体も失敗する可能性があるため、無限の自動ループに陥るのではなく、リトライ、デッドレター、アラート、人的引き継ぎを追加してください。
- グローバルな分散ロックは障害ドメインを拡大させ、リモートでコミットされたビジネス処理を取り消すことはできません。
- 在庫や支払いの保留にはTTL(有効期限)を定義し、Sagaの期限が切れた際にはタイマーまたはイベントを使用して解放してください。
7. 発展トピック
オーケストレーションとコレオグラフィを比較してください。オーケストレーションは状態、順序、タイムアウトを一元管理しますが、コレオグラフィはイベントを通じてサービスを疎結合にする一方で、エンドツーエンドの追跡を難しくします。補償不可能な副作用、セマンティックロック、バージョン競合、監査ログ、および単一のデータベーストランザクションで済むケースについて議論してください。
8. 面接の評価ポイント
ローカルトランザクションを分解できるか
候補者は在庫、支払い、配送、クーポンの各ステップを挙げ、各サービスが自身のローカルトランザクションのみをコミットすることを説明できる必要があります。
補償ステートマシンを設計できるか
成功、失敗、および逆順の補償パスを示し、ビジネス上の拒絶、技術的リトライ、人的引き継ぎを区別して説明できる必要があります。
冪等性と信頼性の高いメッセージングを処理できるか
saga_id、step_id、冪等性キー、およびアウトボックスまたは信頼性の高いキューを使用して、コーディネーターのクラッシュやコマンド送信のギャップに対応できる必要があります。
ビジネスの境界を明示できるか
補償処理がロールバックではないことを認識し、TTL、補償不可能な副作用、ステータス照会、および結果整合性におけるユーザーエクスペリエンスについて議論できる必要があります。