冪等性の設計 - 安全なAPI・処理を実現する

12分 で読める | 2025.12.28

冪等性とは

冪等性(べきとうせい、Idempotency)は、同じ操作を何度実行しても結果が変わらない性質です。分散システムやAPI設計において、信頼性を確保するための重要な概念です。

数学的定義: f(f(x)) = f(x) 同じ関数を複数回適用しても、1回適用したときと同じ結果になる

なぜ冪等性が重要か

ネットワーク通信では、以下のような問題が発生します。

sequenceDiagram
    participant C as クライアント
    participant S as サーバー

    C->>S: リクエスト
    Note over C,S: タイムアウト<br/>(レスポンスが届かなかった)
    Note over C: リクエストは処理された?<br/>リトライすべき?

冪等な操作であれば、安全にリトライできます。

HTTPメソッドと冪等性

メソッド冪等性説明
GETリソース取得、副作用なし
HEADGETと同様(ボディなし)
PUTリソースの完全な置換
DELETEリソースの削除
POSTリソースの作成、副作用あり
PATCH部分更新(実装による)

PUT vs POST

メソッド結果
PUT /users/123何度実行しても、user 123 は同じ状態に
POST /users実行するたびに新しいユーザーが作成される可能性

冪等性キー(Idempotency Key)

POSTリクエストでも冪等性を実現するための手法です。

POST /payments
Idempotency-Key: pay_abc123xyz
Content-Type: application/json

{
  "amount": 5000,
  "currency": "JPY"
}

実装例

async function processPayment(req, res) {
  const idempotencyKey = req.headers['idempotency-key'];

  if (!idempotencyKey) {
    return res.status(400).json({ error: 'Idempotency-Key required' });
  }

  // 既存の処理結果を確認
  const existing = await redis.get(`idempotency:${idempotencyKey}`);
  if (existing) {
    return res.status(200).json(JSON.parse(existing));
  }

  // ロックを取得(同時実行を防ぐ)
  const lock = await acquireLock(idempotencyKey);
  if (!lock) {
    return res.status(409).json({ error: 'Request in progress' });
  }

  try {
    // 処理を実行
    const result = await executePayment(req.body);

    // 結果を保存(24時間有効)
    await redis.setex(
      `idempotency:${idempotencyKey}`,
      86400,
      JSON.stringify(result)
    );

    return res.status(201).json(result);
  } finally {
    await releaseLock(idempotencyKey);
  }
}

冪等性の実装パターン

1. 一意識別子による重複チェック

async function createOrder(orderData, requestId) {
  // 既存チェック
  const existing = await db.orders.findByRequestId(requestId);
  if (existing) {
    return existing; // 同じ結果を返す
  }

  // 新規作成
  const order = await db.orders.create({
    ...orderData,
    requestId // 一意識別子を保存
  });

  return order;
}

2. 状態チェック

async function cancelOrder(orderId) {
  const order = await db.orders.findById(orderId);

  // 既にキャンセル済みなら何もしない
  if (order.status === 'cancelled') {
    return order; // 冪等
  }

  // キャンセル可能な状態か確認
  if (order.status === 'shipped') {
    throw new Error('Cannot cancel shipped order');
  }

  return await db.orders.update(orderId, { status: 'cancelled' });
}

3. 楽観的ロック

async function updateInventory(productId, quantity, version) {
  const result = await db.query(
    `UPDATE inventory
     SET quantity = quantity - $1, version = version + 1
     WHERE product_id = $2 AND version = $3`,
    [quantity, productId, version]
  );

  if (result.rowCount === 0) {
    throw new Error('Concurrent modification detected');
  }
}

冪等でない操作を冪等にする

インクリメント操作

// 冪等でない
UPDATE balance SET amount = amount + 100 WHERE user_id = 123

// 冪等にする(絶対値で設定)
UPDATE balance SET amount = 5100 WHERE user_id = 123

// または、トランザクションIDで管理
INSERT INTO transactions (id, user_id, amount)
VALUES ('tx_abc', 123, 100)
ON CONFLICT (id) DO NOTHING;

UPDATE balance
SET amount = (SELECT SUM(amount) FROM transactions WHERE user_id = 123)
WHERE user_id = 123;

メール送信

async function sendWelcomeEmail(userId, requestId) {
  // 送信済みチェック
  const sent = await db.emailLogs.findOne({
    userId,
    type: 'welcome',
    requestId
  });

  if (sent) {
    return { status: 'already_sent' };
  }

  // 送信
  await emailService.send(/* ... */);

  // ログ記録
  await db.emailLogs.create({
    userId,
    type: 'welcome',
    requestId,
    sentAt: new Date()
  });

  return { status: 'sent' };
}

リトライ戦略

指数バックオフ

async function retryWithBackoff(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;

      // 指数バックオフ + ジッター
      const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
      await sleep(delay);
    }
  }
}

リトライすべきエラー

リトライ種類
✓ 可能5xx エラー(サーバーエラー)、タイムアウト、一時的なネットワークエラー
✗ 不可4xx エラー(クライアントエラー)、ビジネスロジックエラー、認証エラー

まとめ

冪等性は、分散システムやAPIの信頼性を確保するための重要な設計原則です。冪等性キーの導入、状態チェック、楽観的ロックなどのパターンを活用し、安全にリトライできる操作を設計しましょう。

← 一覧に戻る