ストーリー
詳細比較
| 項目 | 2PC | Saga |
|---|---|---|
| 整合性 | 強整合性(即座) | 結果整合性(遅延あり) |
| 分離性 | SERIALIZABLE | 分離なし(対策必要) |
| 可用性 | 低(全参加者の応答必要) | 高(非同期処理可能) |
| パフォーマンス | 低(同期ブロッキング) | 高(非同期並列) |
| 複雑さ | プロトコルは単純 | 補償ロジックが複雑 |
| 障害回復 | Coordinator依存 | 各サービスが自律回復 |
Saga の分離性問題と対策
Saga は複数のローカルトランザクションで構成されるため、ACID の分離性がありません:
Lost Update(更新喪失)
Saga A: 注文の合計金額を計算中
Saga B: 同じ注文にクーポンを適用
→ Saga A の計算結果が Saga B の変更を上書き
対策: セマンティックロック
// 注文レコードにステータスフラグを使って「処理中」を示す
class Order {
status: 'PENDING' | 'PROCESSING' | 'CONFIRMED' | 'CANCELLED';
startProcessing(): void {
if (this.status !== 'PENDING') {
throw new Error('Cannot process: order is ' + this.status);
}
this.status = 'PROCESSING'; // セマンティックロック
}
}
Dirty Read(未コミット読み取り)
Saga A: Step1完了(注文作成)→ Step2実行中(在庫引当)
Saga B: 注文一覧を読み取り → Saga Aの未完了注文が見える
対策: フラグで未確定データをフィルタ
// 確定済みデータのみ返す
async getConfirmedOrders(): Promise<Order[]> {
return this.orderRepo.find({
where: { status: 'CONFIRMED' }, // PROCESSING は除外
});
}
冪等性の実装
分散環境ではメッセージの重複配信は避けられません:
class IdempotentMessageHandler {
constructor(private processedRepo: ProcessedMessageRepository) {}
async handle(message: Message): Promise<void> {
// 1. 既に処理済みか確認
const exists = await this.processedRepo.exists(message.id);
if (exists) {
console.log(`Message ${message.id} already processed, skipping`);
return;
}
// 2. ビジネスロジック実行 + 処理済み記録をアトミックに
await this.db.transaction(async (tx) => {
await this.processBusinessLogic(tx, message);
await this.processedRepo.save(tx, {
messageId: message.id,
processedAt: new Date(),
});
});
}
}
冪等キーの設計
| パターン | 例 | 適用場面 |
|---|---|---|
| イベントID | evt-abc-123 | 全てのイベント処理 |
| ビジネスキー | order-789-payment | 1注文1決済を保証 |
| リクエストID | req-xyz-456 | API呼び出しの重複排除 |
Saga の状態管理
// Saga状態をDBに永続化
interface SagaState {
sagaId: string;
type: 'ORDER_SAGA';
status: 'STARTED' | 'RESERVING' | 'CHARGING' | 'COMPLETED' | 'COMPENSATING' | 'FAILED';
context: Record<string, unknown>; // ビジネスデータ
currentStep: number;
completedSteps: string[];
createdAt: Date;
updatedAt: Date;
}
// Saga状態の永続化により、サービス再起動後も継続可能
class SagaStateRepository {
async save(state: SagaState): Promise<void> {
await this.db.upsert('saga_states', state, { conflictKey: 'sagaId' });
}
async findPending(): Promise<SagaState[]> {
// サービス起動時に未完了のSagaを復旧
return this.db.find('saga_states', {
where: { status: Not(In(['COMPLETED', 'FAILED'])) },
});
}
}
まとめ
| ポイント | 内容 |
|---|---|
| 2PC vs Saga | マイクロサービスでは Saga が推奨 |
| 分離性対策 | セマンティックロックで更新喪失を防止 |
| 冪等性 | メッセージID + 処理済み記録で重複排除 |
| 状態管理 | Saga 状態を DB に永続化して障害回復 |
チェックリスト
- 2PC と Saga の長短を比較できる
- Saga の分離性問題と対策を説明できる
- 冪等性の実装パターンを理解した
- Saga 状態の永続化の重要性を理解した
次のステップへ
次は補償トランザクションの設計パターンを詳しく学びます。
推定読了時間: 40分