分散トランザクションの課題
マイクロサービスでは、1つのビジネス操作が複数のサービスにまたがることがあります。各サービスは独自のデータベースを持つため、従来のACIDトランザクションが使えません。
flowchart LR
Order["1. 注文サービス<br/>注文を作成"]
Inventory["2. 在庫サービス<br/>在庫を減らす"]
Payment["3. 決済サービス<br/>支払いを処理"]
Shipping["4. 配送サービス<br/>配送を手配"]
Order --> Inventory --> Payment --> Shipping
Question["❓ どこかで失敗したら?<br/>全体をロールバック?"]
2相コミット(2PC)
すべての参加者が準備完了してから、一斉にコミットするプロトコルです。
sequenceDiagram
participant C as コーディネーター
participant A as 参加者A
participant B as 参加者B
C->>A: 1. Prepare
C->>B: 1. Prepare
A-->>C: 2. Ready
B-->>C: 2. Ready
C->>A: 3. Commit
C->>B: 3. Commit
A-->>C: 4. Done
B-->>C: 4. Done
2PCの問題点
| 問題 | 説明 |
|---|---|
| ブロッキング | 参加者はコーディネーターの応答を待つ間ロック |
| 単一障害点 | コーディネーターがダウンすると全体が停止 |
| 低パフォーマンス | 同期的な通信が必要 |
| 分散環境での限界 | ネットワーク分断時の対応が困難 |
現代のマイクロサービスでは2PCは推奨されません
Sagaパターン
長時間のトランザクションを、一連のローカルトランザクションに分割します。失敗時は補償トランザクションでロールバックします。
flowchart LR
subgraph Normal["正常フロー"]
T1["T1<br/>注文作成"] --> T2["T2<br/>在庫確保"] --> T3["T3<br/>決済"] --> T4["T4<br/>配送手配"]
end
flowchart LR
subgraph Failure["失敗時(T3で失敗)"]
T1b["T1"] --> T2b["T2"] --> T3b["T3<br/>❌ 失敗"] --> C2["C2<br/>在庫戻し"] --> C1["C1<br/>注文キャンセル"]
end
オーケストレーション方式
中央のオーケストレーターがSagaを制御します。
class OrderSaga {
async execute(orderData) {
const sagaId = uuid();
let currentStep = 0;
try {
// Step 1: 注文作成
const order = await orderService.create(orderData);
currentStep = 1;
// Step 2: 在庫確保
await inventoryService.reserve(order.items);
currentStep = 2;
// Step 3: 決済
await paymentService.process(order.total);
currentStep = 3;
// Step 4: 配送手配
await shippingService.schedule(order);
currentStep = 4;
return { success: true, orderId: order.id };
} catch (error) {
// 補償トランザクションを実行
await this.compensate(currentStep, order);
throw error;
}
}
async compensate(step, order) {
if (step >= 3) await paymentService.refund(order.id);
if (step >= 2) await inventoryService.release(order.items);
if (step >= 1) await orderService.cancel(order.id);
}
}
コレオグラフィ方式
各サービスがイベントを発行・購読して協調します。
flowchart TB
subgraph Success["正常フロー"]
OS1["注文サービス"] --> E1["OrderCreated"]
E1 --> IS1["在庫サービス"] --> E2["InventoryReserved"]
E2 --> PS1["決済サービス"] --> E3["PaymentProcessed"]
E3 --> SS1["配送サービス"] --> E4["ShippingScheduled"]
end
flowchart TB
subgraph Failure["失敗時"]
PS2["決済サービス"] --> EF1["PaymentFailed"]
EF1 --> IS2["在庫サービス"] --> EF2["InventoryReleased<br/>(補償)"]
EF2 --> OS2["注文サービス"] --> EF3["OrderCancelled<br/>(補償)"]
end
比較
| 観点 | オーケストレーション | コレオグラフィ |
|---|---|---|
| 可視性 | フローが明確 | フローが分散 |
| 結合度 | オーケストレーターに依存 | サービス間が疎結合 |
| 複雑さ | 単純なロジック | イベント設計が複雑 |
| デバッグ | 容易 | 困難 |
補償トランザクション
失敗時にロールバックする代わりに、逆の操作を実行します。
// 元の操作
async function reserveInventory(items) {
for (const item of items) {
await db.inventory.update({
where: { productId: item.productId },
data: { quantity: { decrement: item.quantity } }
});
}
}
// 補償トランザクション
async function releaseInventory(items) {
for (const item of items) {
await db.inventory.update({
where: { productId: item.productId },
data: { quantity: { increment: item.quantity } }
});
}
}
補償の考慮点
すべての操作が補償可能とは限らない:
| 操作 | 補償方法 |
|---|---|
| メール送信 | 「取り消しメール」を送信 |
| 物理的な作業開始 | 手動介入が必要 |
| 外部APIコール | 外部システムの補償APIが必要 |
Outboxパターン
データベース更新とイベント発行を確実に行います。
// 1. トランザクション内で両方を実行
async function createOrder(orderData) {
await db.transaction(async (tx) => {
// ビジネスデータを保存
const order = await tx.orders.create(orderData);
// outboxテーブルにイベントを保存
await tx.outbox.create({
eventType: 'OrderCreated',
payload: JSON.stringify(order),
status: 'pending'
});
});
}
// 2. 別プロセスでoutboxをポーリング
async function publishOutboxEvents() {
const events = await db.outbox.findMany({
where: { status: 'pending' }
});
for (const event of events) {
await messageQueue.publish(event.eventType, event.payload);
await db.outbox.update({
where: { id: event.id },
data: { status: 'published' }
});
}
}
結果整合性
即座の整合性ではなく、最終的に整合性が取れることを許容します。
| 整合性モデル | 説明 |
|---|---|
| 強い整合性 | 書き込み直後に全員が同じデータを見る |
| 結果整合性 | 書き込み後、しばらくすると全員が同じデータを見る(一時的な不整合を許容) |
結果整合性の対処
// UI側での対応例
async function placeOrder(orderData) {
const result = await api.createOrder(orderData);
// 即座に確定ではなく「処理中」として表示
return {
orderId: result.orderId,
status: 'processing',
message: 'ご注文を受け付けました。確定後にメールでお知らせします。'
};
}
まとめ
分散トランザクションは、マイクロサービスアーキテクチャにおける難しい課題です。2PCは現代の分散システムには適さず、Sagaパターンと補償トランザクションが推奨されます。結果整合性を受け入れ、ビジネス要件に合った整合性モデルを選択することが重要です。
← 一覧に戻る