冪等性と整合性
冪等性と整合性
Section titled “冪等性と整合性”分散システムでは「1回しか実行されない」は幻想。「何度実行されても最終状態が正しい」ことを保証する設計を詳しく解説します。
冪等性の重要性
Section titled “冪等性の重要性”分散システムでは、ネットワークエラー、タイムアウト、再起動などにより、同じ処理が複数回実行される可能性があります。
問題のあるコード
Section titled “問題のあるコード”// ❌ 問題のあるコード: 非冪等な処理async function createOrder(orderData: OrderData): Promise<Order> { // 問題: 再実行時に注文が二重作成される return await prisma.order.create({ data: orderData });}なぜ問題か:
- 再実行時の二重作成: ネットワークエラーでクライアントが再送すると、注文が2つ作成される
- データの不整合: 同じ注文が複数存在し、在庫や決済に影響する
冪等性の担保
Section titled “冪等性の担保”Idempotency Keyの使用
Section titled “Idempotency Keyの使用”// ✅ 良い例: Idempotency Keyによる冪等性の担保async function createOrder( orderData: OrderData, idempotencyKey: string): Promise<Order> { // Idempotency Keyで既存の注文を確認 const existingOrder = await prisma.order.findUnique({ where: { idempotencyKey }, });
if (existingOrder) { // 既に存在する場合は、既存の注文を返す return existingOrder; }
// 新規作成 return await prisma.order.create({ data: { ...orderData, idempotencyKey, // 一意制約で重複を防止 }, });}
// スキーマにIdempotency Keyを追加model Order { id Int @id @default(autoincrement()) idempotencyKey String @unique amount Decimal // ...}なぜ重要か:
- 重複防止: 同じIdempotency Keyで再実行しても、同じ結果が返される
- データの整合性: 注文の重複作成を防止
トランザクション境界
Section titled “トランザクション境界”DB取引中に外部APIを呼ばない(失敗時復旧不可)。
問題のあるコード
Section titled “問題のあるコード”// ❌ 問題のあるコード: トランザクション内で外部APIを呼ぶasync function createOrder(orderData: OrderData): Promise<Order> { return await prisma.$transaction(async (tx) => { // 1. 注文を作成(DBトランザクション内) const order = await tx.order.create({ data: orderData });
// 2. トランザクション内で外部APIを呼ぶ(問題) const response = await fetch('https://payment-api.example.com/charge', { method: 'POST', body: JSON.stringify({ orderId: order.id, amount: orderData.amount, }), });
if (!response.ok) { throw new Error('Payment failed'); }
// 3. 決済結果を保存 await tx.order.update({ where: { id: order.id }, data: { paymentStatus: 'COMPLETED' }, });
return order; });}なぜ問題か:
- ロールバック不可: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
- データの不整合: 外部APIは成功しているが、DBには注文が存在しない状態になる可能性
Outboxパターンの実装
Section titled “Outboxパターンの実装”// ✅ 良い例: Outboxパターンによる解決async function createOrder( orderData: OrderData, idempotencyKey: string): Promise<Order> { return await prisma.$transaction(async (tx) => { // 1. Idempotency Keyで既存の注文を確認 const existingOrder = await tx.order.findUnique({ where: { idempotencyKey }, });
if (existingOrder) { return existingOrder; }
// 2. トランザクション内で注文を作成 const order = await tx.order.create({ data: { ...orderData, idempotencyKey, }, });
// 3. Outboxテーブルに外部API呼び出しを記録(トランザクション内) await tx.outbox.create({ data: { eventType: 'PAYMENT_CHARGE', aggregateId: order.id.toString(), payload: JSON.stringify({ orderId: order.id, amount: orderData.amount, }), idempotencyKey, status: 'PENDING', }, });
// 4. トランザクションをコミット(外部APIは呼ばない) return order; });}
// 別プロセスでOutboxを処理setInterval(async () => { const pendingEvents = await prisma.outbox.findMany({ where: { status: 'PENDING' }, take: 10, });
for (const event of pendingEvents) { try { // 外部APIを呼ぶ(トランザクション外) const payload = JSON.parse(event.payload); const response = await fetch('https://payment-api.example.com/charge', { method: 'POST', headers: { 'Idempotency-Key': event.idempotencyKey, }, body: JSON.stringify(payload), });
if (response.ok) { await prisma.outbox.update({ where: { id: event.id }, data: { status: 'COMPLETED' }, }); } else { await prisma.outbox.update({ where: { id: event.id }, data: { status: 'FAILED', retryCount: { increment: 1 }, }, }); } } catch (error) { await prisma.outbox.update({ where: { id: event.id }, data: { status: 'FAILED', retryCount: { increment: 1 }, }, }); } }}, 5000);なぜ重要か:
- トランザクションの短縮: DBのロック時間が短縮される
- 外部障害の分離: 外部APIの障害がトランザクションに影響しない
- 再実行の容易さ: Outboxテーブルから再実行可能
- 冪等性の保証: Idempotency Keyにより重複実行を防止
再送安全なフロー
Section titled “再送安全なフロー”「結果整合性で良いデータ」と「厳密整合性が必要なデータ」を明示的に分類する。
整合性レベルの分類
Section titled “整合性レベルの分類”// 厳密整合性が必要なデータ(ACIDトランザクション)interface Order { id: number; amount: number; status: 'CREATED' | 'PAID' | 'CANCELLED';}
// 結果整合性で良いデータ(イベント駆動)interface OrderAnalytics { id: number; orderId: number; totalAmount: number; // 集計値(最終的に整合性が取れれば良い) lastUpdated: Date;}使い分け:
- 厳密整合性: 注文、決済、在庫など、ビジネス的に重要なデータ
- 結果整合性: 分析データ、ログ、通知など、最終的に整合性が取れれば良いデータ
冪等性と整合性のポイント:
- 冪等性の担保: Idempotency Keyで再実行を安全化
- トランザクション境界: DB取引中に外部APIを呼ばない(Outboxパターンを使用)
- 再送安全なフロー: 厳密整合性と結果整合性を明示的に分類
これらの原則により、「何度実行されても最終状態が正しい」堅牢なシステムを構築できます。