ストーリー
「注文サービスは成功したのに、在庫サービスが失敗して在庫が減ってない。でも注文は確定状態になってる…」
マイクロサービスに移行した後、こうした整合性の問題が増えた。高橋アーキテクトが説明する。
「単一DBならBEGIN-COMMITで済む。しかし複数サービス、複数DBにまたがるトランザクションは根本的に難しい。2PCやSagaパターンで対処するんだ」
なぜ分散トランザクションが必要か
モノリス → マイクロサービス
モノリス:
[注文処理]
BEGIN;
INSERT INTO orders ...
UPDATE inventory SET stock = stock - 1 ...
INSERT INTO payments ...
COMMIT;
→ 1つのDBトランザクションで完結
マイクロサービス:
[注文サービス] → 注文DB
[在庫サービス] → 在庫DB
[決済サービス] → 決済DB
→ 3つのDBにまたがる。1つのCOMMITでは済まない
2PC(Two-Phase Commit)
コーディネーターが全参加者の合意を取って一斉にコミットする。
Phase 1: Prepare(準備)
Coordinator → Service A: "コミットできるか?"
Coordinator → Service B: "コミットできるか?"
Coordinator → Service C: "コミットできるか?"
Service A → Coordinator: "Yes, 準備完了"
Service B → Coordinator: "Yes, 準備完了"
Service C → Coordinator: "Yes, 準備完了"
Phase 2: Commit(確定)
全員 Yes の場合:
Coordinator → Service A: "コミットせよ"
Coordinator → Service B: "コミットせよ"
Coordinator → Service C: "コミットせよ"
1つでも No の場合:
Coordinator → All: "ロールバックせよ"
2PCの問題点
| 問題 | 説明 |
|---|---|
| ブロッキング | Prepare後にコーディネーターがダウンすると全参加者がブロック |
| パフォーマンス | 全サービスのレスポンスを待つため遅い |
| 可用性の低下 | 1つのサービスがダウンすると全体が止まる |
| 複雑さ | リカバリロジックが複雑 |
Sagaパターン
分散トランザクションを一連のローカルトランザクションに分解し、失敗時は補償トランザクション(undo操作)で巻き戻す。
Choreography(振り付け)方式
各サービスがイベントを発行し、次のサービスがリアクションする。
1. 注文サービス: 注文作成 → "OrderCreated" イベント発行
2. 在庫サービス: 在庫確保 → "InventoryReserved" イベント発行
3. 決済サービス: 決済処理 → "PaymentProcessed" イベント発行
4. 注文サービス: 注文確定
失敗時(決済失敗):
3. 決済サービス: 失敗 → "PaymentFailed" イベント発行
2. 在庫サービス: 在庫戻し(補償トランザクション)
1. 注文サービス: 注文キャンセル(補償トランザクション)
// Choreography方式の実装イメージ
// 注文サービス
class OrderService {
async createOrder(orderData: OrderInput): Promise<Order> {
// ローカルトランザクション
const order = await this.orderRepo.create({
...orderData,
status: 'pending',
});
// イベント発行
await this.eventBus.publish('OrderCreated', {
orderId: order.id,
items: order.items,
userId: order.userId,
});
return order;
}
// 補償トランザクション
async handlePaymentFailed(event: PaymentFailedEvent): Promise<void> {
await this.orderRepo.updateStatus(event.orderId, 'cancelled');
}
}
// 在庫サービス
class InventoryService {
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
try {
await this.reserveStock(event.items);
await this.eventBus.publish('InventoryReserved', {
orderId: event.orderId,
});
} catch (error) {
await this.eventBus.publish('InventoryReservationFailed', {
orderId: event.orderId,
reason: error.message,
});
}
}
// 補償トランザクション
async handlePaymentFailed(event: PaymentFailedEvent): Promise<void> {
await this.releaseStock(event.orderId);
}
}
Orchestration(指揮)方式
中央のオーケストレーターがすべてのステップを制御する。
// Orchestration方式の実装
class OrderSagaOrchestrator {
async execute(orderData: OrderInput): Promise<OrderResult> {
const sagaId = generateId();
const steps: SagaStep[] = [];
try {
// Step 1: 注文作成
const order = await this.orderService.createOrder(orderData);
steps.push({ service: 'order', action: 'create', data: order });
// Step 2: 在庫確保
await this.inventoryService.reserveStock(order.items);
steps.push({ service: 'inventory', action: 'reserve', data: order.items });
// Step 3: 決済処理
await this.paymentService.processPayment(order);
steps.push({ service: 'payment', action: 'charge', data: order });
// Step 4: 注文確定
await this.orderService.confirmOrder(order.id);
return { success: true, orderId: order.id };
} catch (error) {
// 補償トランザクション(逆順で実行)
await this.compensate(steps);
return { success: false, error: error.message };
}
}
private async compensate(steps: SagaStep[]): Promise<void> {
// 逆順で補償処理
for (const step of steps.reverse()) {
try {
switch (`${step.service}:${step.action}`) {
case 'payment:charge':
await this.paymentService.refund(step.data);
break;
case 'inventory:reserve':
await this.inventoryService.releaseStock(step.data);
break;
case 'order:create':
await this.orderService.cancelOrder(step.data.id);
break;
}
} catch (compensationError) {
// 補償トランザクション自体の失敗 → 要手動対応
await this.alertService.notifyCompensationFailure(step, compensationError);
}
}
}
}
Choreography vs Orchestration
| 特性 | Choreography | Orchestration |
|---|---|---|
| 結合度 | 低い(イベント駆動) | やや高い(中央制御) |
| 可視性 | 低い(フロー追跡が困難) | 高い(全体が1箇所で見える) |
| 複雑さ | サービス数増で複雑化 | ロジック集中で管理しやすい |
| 障害対応 | 各サービスで個別対応 | オーケストレーターで一元管理 |
| 適用規模 | 2-3ステップの単純なSaga | 4ステップ以上の複雑なSaga |
Transactional Outbox パターン
ローカルトランザクションとイベント発行の原子性を保証する。
-- 同一トランザクション内でデータ更新とイベント格納
BEGIN;
INSERT INTO orders (id, user_id, status) VALUES (1, 100, 'pending');
INSERT INTO outbox_events (
aggregate_type, aggregate_id, event_type, payload
) VALUES (
'Order', 1, 'OrderCreated',
'{"orderId": 1, "userId": 100}'
);
COMMIT;
-- 別プロセスがoutbox_eventsをポーリングしてイベントを発行
-- 発行後にイベントを削除(またはマーク)
まとめ
| パターン | 特徴 | 適用場面 |
|---|---|---|
| 2PC | 強い整合性、ブロッキング | 同一組織内の少数のDB |
| Saga (Choreography) | イベント駆動、疎結合 | 2-3ステップの単純なフロー |
| Saga (Orchestration) | 中央制御、可視性高い | 複雑な複数ステップ |
| Transactional Outbox | イベント発行の信頼性保証 | Saga と組み合わせ |
理解度チェックリスト
- 2PCの2フェーズの流れを説明できる
- 2PCの問題点を理解している
- Sagaパターンの補償トランザクションを設計できる
- ChoreographyとOrchestrationの使い分けを判断できる
次のステップ
次のレッスンではイベントソーシングとCQRSを学ぶ。状態ではなくイベント(出来事)を記録する設計パターンで、監査証跡と柔軟なデータ活用を実現する。
推定読了時間: 40分