冪等性の設計 - 安全な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の信頼性を確保するための重要な設計原則です。冪等性キーの導入、状態チェック、楽観的ロックなどのパターンを活用し、安全にリトライできる操作を設計しましょう。

参考リソース

← 一覧に戻る
PR
PR
PR
PR