Skip to content

冪等性と整合性

分散システムでは「1回しか実行されない」は幻想。「何度実行されても最終状態が正しい」ことを保証する設計を詳しく解説します。

分散システムでは、ネットワークエラー、タイムアウト、再起動などにより、同じ処理が複数回実行される可能性があります。

// ❌ 問題のあるコード: 非冪等な処理
async function createOrder(orderData: OrderData): Promise<Order> {
// 問題: 再実行時に注文が二重作成される
return await prisma.order.create({ data: orderData });
}

なぜ問題か:

  • 再実行時の二重作成: ネットワークエラーでクライアントが再送すると、注文が2つ作成される
  • データの不整合: 同じ注文が複数存在し、在庫や決済に影響する
// ✅ 良い例: 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で再実行しても、同じ結果が返される
  • データの整合性: 注文の重複作成を防止

DB取引中に外部APIを呼ばない(失敗時復旧不可)。

// ❌ 問題のあるコード: トランザクション内で外部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パターンによる解決
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により重複実行を防止

「結果整合性で良いデータ」と「厳密整合性が必要なデータ」を明示的に分類する。

// 厳密整合性が必要なデータ(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パターンを使用)
  • 再送安全なフロー: 厳密整合性と結果整合性を明示的に分類

これらの原則により、「何度実行されても最終状態が正しい」堅牢なシステムを構築できます。