Skip to content

冪等性と整合性

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

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

# ❌ 問題のあるコード: 非冪等な処理
class OrderService
def create_order(order_data)
# 問題: 再実行時に注文が二重作成される
Order.create!(order_data)
end
end

なぜ問題か:

  • 再実行時の二重作成: ネットワークエラーでクライアントが再送すると、注文が2つ作成される
  • データの不整合: 同じ注文が複数存在し、在庫や決済に影響する
# ✅ 良い例: Idempotency Keyによる冪等性の担保
class OrderService
def create_order(order_data, idempotency_key)
# Idempotency Keyで既存の注文を確認
existing_order = Order.find_by(idempotency_key: idempotency_key)
return existing_order if existing_order
# 新規作成
Order.create!(
order_data.merge(idempotency_key: idempotency_key)
)
end
end
# マイグレーションでIdempotency Keyを追加
class AddIdempotencyKeyToOrders < ActiveRecord::Migration[7.0]
def change
add_column :orders, :idempotency_key, :string
add_index :orders, :idempotency_key, unique: true
end
end

なぜ重要か:

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

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

# ❌ 問題のあるコード: トランザクション内で外部APIを呼ぶ
class OrderService
def create_order(order_data)
Order.transaction do
# 1. 注文を作成(DBトランザクション内)
order = Order.create!(order_data)
# 2. トランザクション内で外部APIを呼ぶ(問題)
result = PaymentService.new.charge_payment(order.id, order_data[:amount])
raise PaymentError, 'Payment failed' unless result.success?
# 3. 決済結果を保存
order.update!(payment_status: 'COMPLETED')
order
end
end
end

なぜ問題か:

  • ロールバック不可: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
  • データの不整合: 外部APIは成功しているが、DBには注文が存在しない状態になる可能性
# ✅ 良い例: Outboxパターンによる解決
class OrderService
def create_order(order_data, idempotency_key)
Order.transaction do
# 1. Idempotency Keyで既存の注文を確認
existing_order = Order.find_by(idempotency_key: idempotency_key)
return existing_order if existing_order
# 2. トランザクション内で注文を作成
order = Order.create!(
order_data.merge(idempotency_key: idempotency_key)
)
# 3. Outboxテーブルに外部API呼び出しを記録(トランザクション内)
OutboxEvent.create!(
event_type: 'PAYMENT_CHARGE',
aggregate_id: order.id.to_s,
payload: {
order_id: order.id,
amount: order_data[:amount]
}.to_json,
idempotency_key: idempotency_key,
status: 'PENDING'
)
# 4. トランザクションをコミット(外部APIは呼ばない)
order
end
end
end
# 別プロセスでOutboxを処理
class OutboxProcessor
def self.process_pending_events
OutboxEvent.where(status: 'PENDING').limit(10).find_each do |event|
begin
# 外部APIを呼ぶ(トランザクション外)
payload = JSON.parse(event.payload)
# Idempotency Keyをヘッダーに含める
result = PaymentService.new.charge_payment(
payload['order_id'],
payload['amount'],
event.idempotency_key
)
if result.success?
event.update!(status: 'COMPLETED')
else
event.update!(
status: 'FAILED',
retry_count: event.retry_count + 1
)
end
rescue => e
event.update!(
status: 'FAILED',
retry_count: event.retry_count + 1
)
Rails.logger.error("Failed to process outbox event: #{event.id}", e)
end
end
end
end
# 定期的にOutboxを処理
# config/schedule.rb
every 5.seconds do
runner 'OutboxProcessor.process_pending_events'
end

なぜ重要か:

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

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

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

使い分け:

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

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

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

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