冪等性とは
冪等性(べきとうせい、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 | ✓ | リソース取得、副作用なし |
| HEAD | ✓ | GETと同様(ボディなし) |
| 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の信頼性を確保するための重要な設計原則です。冪等性キーの導入、状態チェック、楽観的ロックなどのパターンを活用し、安全にリトライできる操作を設計しましょう。
← 一覧に戻る