ストーリー
フードデリバリーシステムが動き始めたある日、障害レポートが上がりました。
モノリスでのトランザクション
モノリスでは、1つのデータベースでACIDトランザクションが使えます。
// モノリスでは簡単
async function createOrder(data: OrderData): Promise<Order> {
return await db.transaction(async (tx) => {
// 1. 注文を作成
const order = await tx.insert("orders", data);
// 2. 在庫を減らす
await tx.update("inventory",
{ quantity: sql`quantity - ${data.quantity}` },
{ productId: data.productId }
);
// 3. 決済を記録
await tx.insert("payments", {
orderId: order.id,
amount: data.totalAmount,
status: "COMPLETED",
});
return order;
// すべて成功 or すべて失敗(ACID保証)
});
}
マイクロサービスでの問題
マイクロサービスでは、各サービスが独自のデータベースを持つため、単一のACIDトランザクションは使えません。
Order Service Payment Service Inventory Service
[Order DB] [Payment DB] [Inventory DB]
┌─ 注文作成 ─────── 決済処理 ─────── 在庫確保 ─┐
│ │
│ ← この3つの操作をアトミックにしたい │
│ でも3つの異なるDBにまたがっている │
└──────────────────────────────────────────────┘
2フェーズコミット(2PC)の限界
従来の分散トランザクション解決策である2PCは、マイクロサービスには不向きです。
2PC(Two-Phase Commit):
Phase 1: Prepare(準備)
Coordinator → Order DB: "コミットできる?" → "OK"
Coordinator → Payment DB: "コミットできる?" → "OK"
Coordinator → Inventory DB: "コミットできる?" → "OK"
Phase 2: Commit(確定)
Coordinator → Order DB: "コミットしろ"
Coordinator → Payment DB: "コミットしろ"
Coordinator → Inventory DB: "コミットしろ"
2PCの問題点
| 問題 | 説明 |
|---|---|
| 同期的ブロッキング | Prepare〜Commit間、全参加者がロックを保持 |
| 単一障害点 | Coordinatorがダウンすると全体が停止 |
| パフォーマンス | ロック保持時間が長く、スループット低下 |
| 可用性 | 1つの参加者がダウンすると全体がロールバック |
| サービス自律性 | 各サービスが同じトランザクションプロトコルをサポート必要 |
// 2PCの問題を示す例
// Coordinatorがフェーズ2で障害→一部コミット、一部未コミット
// → データ不整合が発生する可能性
// さらに、マイクロサービスでは:
// - HTTP/RESTで2PCプロトコルを実装するのは非現実的
// - NoSQL DBは2PCをサポートしないものが多い
// - サービスの独立デプロイが阻害される
なぜSagaパターンが必要か
2PCの代替として、Sagaパターンが登場しました。
2PC: 全部成功するまでロックして待つ(悲観的)
Saga: 各ステップを順に実行し、失敗したら補償する(楽観的)
Saga の基本的な流れ:
T1 → T2 → T3 → 完了!
T1 → T2 → T3(失敗) → C3 → C2 → C1 → ロールバック完了
(C = 補償トランザクション)
// Sagaの概念
interface SagaStep {
// 正常系の処理
execute(): Promise<void>;
// 補償処理(undo)
compensate(): Promise<void>;
}
// 注文Sagaの例
const orderSaga: SagaStep[] = [
{
execute: () => orderService.createOrder(data), // T1: 注文作成
compensate: () => orderService.cancelOrder(orderId), // C1: 注文キャンセル
},
{
execute: () => paymentService.charge(payment), // T2: 決済
compensate: () => paymentService.refund(paymentId), // C2: 返金
},
{
execute: () => inventoryService.reserve(items), // T3: 在庫確保
compensate: () => inventoryService.release(items), // C3: 在庫解放
},
];
Sagaの特性
| 特性 | 2PC | Saga |
|---|---|---|
| 一貫性 | 強い一貫性(ACID) | 結果整合性 |
| ロック | 長時間ロック | ロックなし |
| 可用性 | 低い | 高い |
| 複雑さ | プロトコルが複雑 | 補償ロジックが複雑 |
| スケーラビリティ | 低い | 高い |
ACD特性(ACIDからIsolationを除外)
Sagaは以下の特性を持ちます。
A(Atomicity): 全ステップ成功 or 全補償実行
C(Consistency): 結果整合性(途中状態が見える)
D(Durability): 各ステップの結果は永続化される
※ I(Isolation)はない → 途中状態が他から見える
中間状態の可視性問題
// Sagaの途中状態が見える問題
// T1: 注文作成(PENDING)
// T2: 決済処理中......
// この時点で別のリクエストが注文を参照すると
// → status = "PENDING" が見える
// もしT2が失敗してC1が実行されると
// → status = "CANCELLED" に変わる
// つまり、一時的に不整合な中間状態が外部に見える
// → これが「結果整合性」の意味
// 対策: セマンティックロック
interface Order {
id: string;
status: "PENDING" | "CONFIRMED" | "CANCELLED"; // 中間状態を明示
sagaStatus: "IN_PROGRESS" | "COMPLETED" | "COMPENSATING";
}
まとめ
| ポイント | 内容 |
|---|---|
| モノリスとの違い | ACIDトランザクションが使えない |
| 2PCの限界 | ブロッキング、単一障害点、低パフォーマンス |
| Sagaの基本 | 各ステップを順に実行、失敗時は補償 |
| トレードオフ | 強い一貫性を諦め、結果整合性を受け入れる |
チェックリスト
- マイクロサービスでACIDトランザクションが使えない理由を説明できる
- 2PCの問題点を3つ以上挙げられる
- Sagaパターンの基本的な流れを説明できる
- 結果整合性の意味を理解した
次のステップへ
次はSagaパターンの2つのタイプ「Choreography」と「Orchestration」を学びます。それぞれの特徴と使い分けを理解しましょう。
推定読了時間: 40分