Skip to content

冪等性と整合性

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

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

// ❌ 問題のあるコード: 非冪等な処理
class OrderService
{
public function createOrder($orderData)
{
// 問題: 再実行時に注文が二重作成される
return Order::create($orderData);
}
}

なぜ問題か:

  • 再実行時の二重作成: ネットワークエラーでクライアントが再送すると、注文が2つ作成される
  • データの不整合: 同じ注文が複数存在し、在庫や決済に影響する
// ✅ 良い例: Idempotency Keyによる冪等性の担保
class OrderService
{
public function createOrder($orderData, $idempotencyKey)
{
// Idempotency Keyで既存の注文を確認
$existingOrder = Order::where('idempotency_key', $idempotencyKey)->first();
if ($existingOrder) {
// 既に存在する場合は、既存の注文を返す
return $existingOrder;
}
// 新規作成
return Order::create(array_merge($orderData, [
'idempotency_key' => $idempotencyKey,
]));
}
}
// マイグレーションでIdempotency Keyを追加
Schema::table('orders', function (Blueprint $table) {
$table->string('idempotency_key')->unique()->after('id');
});

なぜ重要か:

  • 重複防止: 同じIdempotency Keyで再実行しても、同じ結果が返される
  • データの整合性: 注文の重複作成を防止

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

// ❌ 問題のあるコード: トランザクション内で外部APIを呼ぶ
class OrderService
{
public function createOrder($orderData)
{
return DB::transaction(function () use ($orderData) {
// 1. 注文を作成(DBトランザクション内)
$order = Order::create($orderData);
// 2. トランザクション内で外部APIを呼ぶ(問題)
$response = Http::post('https://payment-api.example.com/charge', [
'order_id' => $order->id,
'amount' => $orderData['amount'],
]);
if (!$response->successful()) {
throw new PaymentException('Payment failed');
}
// 3. 決済結果を保存
$order->update(['payment_status' => 'COMPLETED']);
return $order;
});
}
}

なぜ問題か:

  • ロールバック不可: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
  • データの不整合: 外部APIは成功しているが、DBには注文が存在しない状態になる可能性
// ✅ 良い例: Outboxパターンによる解決
class OrderService
{
public function createOrder($orderData, $idempotencyKey)
{
return DB::transaction(function () use ($orderData, $idempotencyKey) {
// 1. Idempotency Keyで既存の注文を確認
$existingOrder = Order::where('idempotency_key', $idempotencyKey)->first();
if ($existingOrder) {
return $existingOrder;
}
// 2. トランザクション内で注文を作成
$order = Order::create(array_merge($orderData, [
'idempotency_key' => $idempotencyKey,
]));
// 3. Outboxテーブルに外部API呼び出しを記録(トランザクション内)
OutboxEvent::create([
'event_type' => 'PAYMENT_CHARGE',
'aggregate_id' => (string)$order->id,
'payload' => json_encode([
'order_id' => $order->id,
'amount' => $orderData['amount'],
]),
'idempotency_key' => $idempotencyKey,
'status' => 'PENDING',
]);
// 4. トランザクションをコミット(外部APIは呼ばない)
return $order;
});
}
}
// 別プロセスでOutboxを処理
class OutboxProcessor
{
public static function processPendingEvents()
{
$pendingEvents = OutboxEvent::where('status', 'PENDING')
->limit(10)
->get();
foreach ($pendingEvents as $event) {
try {
// 外部APIを呼ぶ(トランザクション外)
$payload = json_decode($event->payload, true);
$response = Http::withHeaders([
'Idempotency-Key' => $event->idempotency_key,
])->post('https://payment-api.example.com/charge', $payload);
if ($response->successful()) {
$event->update(['status' => 'COMPLETED']);
} else {
$event->increment('retry_count');
$event->update(['status' => 'FAILED']);
}
} catch (\Exception $e) {
$event->increment('retry_count');
$event->update(['status' => 'FAILED']);
Log::error("Failed to process outbox event: {$event->id}", ['exception' => $e]);
}
}
}
}
// 定期的にOutboxを処理
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->call(function () {
OutboxProcessor::processPendingEvents();
})->everyFiveSeconds();
}

なぜ重要か:

  • トランザクションの短縮: DBのロック時間が短縮される
  • 外部障害の分離: 外部APIの障害がトランザクションに影響しない
  • 再実行の容易さ: Outboxテーブルから再実行可能
  • 冪等性の保証: Idempotency Keyにより重複実行を防止

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

// 厳密整合性が必要なデータ(ACIDトランザクション)
class Order extends Model
{
// 注文、決済、在庫など、ビジネス的に重要なデータ
}
// 結果整合性で良いデータ(イベント駆動)
class OrderAnalytics extends Model
{
// 分析データ、ログ、通知など、最終的に整合性が取れれば良いデータ
// 集計値は最終的に整合性が取れれば良い
}

使い分け:

  • 厳密整合性: 注文、決済、在庫など、ビジネス的に重要なデータ
  • 結果整合性: 分析データ、ログ、通知など、最終的に整合性が取れれば良いデータ

冪等性と整合性のポイント:

  • 冪等性の担保: Idempotency Keyで再実行を安全化
  • トランザクション境界: DB取引中に外部APIを呼ばない(Outboxパターンを使用)
  • 再送安全なフロー: 厳密整合性と結果整合性を明示的に分類

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