「注文は確定したのに、決済は失敗し、在庫だけが減っている」。モノリスからマイクロサービスアーキテクチャ(MSA)へ移行した直後、開発チームを襲う最も恐ろしい悪夢がこれです。データベースがサービスごとに分割された瞬間、かつて @Transactional ひとつで守られていたACID特性は崩壊します。ネットワークの遅延、部分的な障害、そして非同期通信の複雑さが絡み合う分散システムにおいて、データ整合性をどのように担保すべきか?本記事では、机上の空論ではなく、実際の決済システムで発生した整合性不全を解決するための「Sagaパターン」の実装戦略を解説します。
なぜ2PC(2相コミット)では解決できないのか
金融系プロジェクトにおいて、私たちは当初、分散トランザクションの古典的な解法である2PC (Two-Phase Commit) を検討しました。しかし、数千TPSを超えるトラフィック環境下では、2PCは致命的なボトルネックとなります。
高可用性が求められるモダンなMSAにおいて、ロックを長時間保持する手法は採用できません。そこで必要になるのが、BASE特性(Basically Available, Soft state, Eventual consistency) に基づく「結果整合性」のアプローチ、すなわちSagaパターンです。
Sagaパターンの実装戦略:オーケストレーション対コレオグラフィ
Sagaパターンは、ロングランニングトランザクションを複数の小さなローカルトランザクションに分割し、失敗時には「補償トランザクション(Compensating Transaction)」を実行してロールバックを行います。実装には主に2つのアプローチがあります。
1. コレオグラフィ(Choreography)
各サービスがイベントをPub/Subし、自律的に動作します。中央管理者は存在しません。
- メリット: 単一障害点(SPOF)がない、疎結合。
- デメリット: 処理の流れが把握しづらい(循環依存のリスク)。
2. オーケストレーション(Orchestration)推奨
中央の「オーケストレーター」が各サービスにコマンドを発行し、状態を管理します。障害発生時のリカバリ制御が容易であるため、ここではオーケストレーションパターンの実装例を見ていきます。
実装コード:Orchestratorによる補償トランザクション制御
以下は、Spring Boot環境を想定したSagaオーケストレーターの簡略化された実装です。注文作成プロセスにおいて、在庫確保に成功した後、決済で失敗した場合に在庫を戻すロジックを示しています。
// 注文処理のSagaオーケストレーター
@Service
@RequiredArgsConstructor
public class OrderSagaOrchestrator {
private final InventoryClient inventoryClient;
private final PaymentClient paymentClient;
private final OrderRepository orderRepository;
public void processOrder(OrderContext context) {
// Step 1: ローカルトランザクション(注文作成 / PENDING状態)
Order order = createPendingOrder(context);
try {
// Step 2: 在庫引当(外部サービス呼び出し)
// 注意: ネットワークタイムアウト時は冪等性を担保したリトライが必要
boolean inventoryReserved = inventoryClient.reserve(order.getId(), order.getItems());
if (!inventoryReserved) {
markOrderAsFailed(order, "INVENTORY_UNAVAILABLE");
return;
}
// Step 3: 決済処理
boolean paymentSuccess = paymentClient.charge(order.getId(), order.getTotalAmount());
if (!paymentSuccess) {
// !!!障害発生!!! -> 補償トランザクションの実行
log.warn("Payment failed for Order ID: {}. Initiating compensation.", order.getId());
compensateInventory(order); // 在庫を戻す
markOrderAsFailed(order, "PAYMENT_FAILED");
} else {
// 全ステップ成功
completeOrder(order);
}
} catch (Exception e) {
// 想定外のエラー(ネットワーク断絶など)
// リトライキューに入れるか、手動リカバリログを出力
handleSystemFailure(order, e);
}
}
private void compensateInventory(Order order) {
try {
// 補償アクション: 在庫の解放
inventoryClient.release(order.getId(), order.getItems());
} catch (Exception e) {
// 補償自体が失敗した場合 -> 重大な不整合
// "Zombie Saga" としてアラートを発報し、運用担当者が介入する
log.error("CRITICAL: Compensation failed for Order ID: {}. Manual intervention required.", order.getId());
}
}
}
compensateInventory)自体が失敗する可能性があります。この場合、無限リトライを行うか、Dead Letter Queue (DLQ) に送り、運用チームへ即時通知する仕組み(Human-in-the-loop)が不可欠です。
パフォーマンスと複雑性の比較
従来の一貫性モデルとSagaパターンを導入した場合のトレードオフを整理しました。Sagaは複雑性を代償に、可用性とスケーラビリティを手に入れます。
| 評価項目 | 2相コミット (2PC) | Saga (Orchestration) | Saga (Choreography) |
|---|---|---|---|
| 整合性 | 強整合性 (ACID) | 結果整合性 (BASE) | 結果整合性 (BASE) |
| スループット | 低 (ロックにより低下) | 高 | 極めて高 |
| 実装難易度 | 低 (フレームワーク依存) | 中〜高 | 高 (追跡が困難) |
| 結合度 | 密結合 | 中 (オーケストレーターに依存) | 疎結合 |
correlation_id(相関ID)をログに含めることで、分散トレーシング(ZipkinやJaeger)によるデバッグが可能になります。
結論
マイクロサービスにおけるデータ整合性は、技術的な銀の弾丸が存在しない領域です。Sagaパターン、特にオーケストレーションアプローチは、複雑なビジネスフローを制御下におくための強力な武器ですが、補償トランザクションの設計漏れは新たなデータ不整合を生みます。
成功の鍵は、「失敗は必ず起こる」という前提で設計することです。冪等性の担保、リトライ戦略、そして補償失敗時の運用フローまでを含めて設計して初めて、悪夢から解放された堅牢な分散システムが完成します。
Post a Comment