データベースに到達する前に重複アクションを阻止する
顧客が「今すぐ支払う」をクリックします。読み込み中のアイコンが3秒間回っています。パニックになった顧客は、ボタンをさらに4回連打します。対策がなければ、サーバーはそのトランザクションを繰り返し処理してしまい、1つの注文に対して銀行口座から何度も引き落としを行ってしまうかもしれません。ここで、冪等性(べきとうせい/Idempotency)がバックエンドにおける最も重要な機能となります。
冪等な操作とは、同じ操作を何度行っても同じ結果が得られることを保証するものです。REST APIにおいては、タイムアウトなどでクライアントがリクエストを再試行した場合、サーバーがそれを認識して正確に一度だけ処理し、それ以降のすべての試行に対しては最初と同じ成功レスポンスを返すことを意味します。
失敗した場合の代償
私は、この保護がないシステムで、わずかなマイクロサービスの不具合により、120ドルのサブスクリプションに対してユーザーに480ドルを請求してしまった例を見てきました。その一度の間違いを解決するために、6時間の手動によるデータベース修正と、サポートリーダーからの正式な謝罪が必要でした。冪等性の習得は単なる技術的な好みではなく、データの整合性とユーザーの信頼を守るためのものです。
最小限の実装
開始するには、Node.jsとRedisインスタンスがあれば十分です。Redisはキーの有効期限を自動的に処理し、1ミリ秒未満のレイテンシで動作するため、この用途では業界標準となっています。
npm install express ioredis
次の例では、ioredisを使用して、ビジネスロジックを実行する前に一意のキーをチェックしています。
const express = require('express');
const Redis = require('ioredis');
const redis = new Redis();
const app = express();
app.use(express.json());
app.post('/payments', async (req, res) => {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({ error: 'Idempotency-Keyヘッダーが必要です' });
}
// まずキャッシュされたレスポンスを探す
const cachedResponse = await redis.get(`idempotency:${idempotencyKey}`);
if (cachedResponse) {
return res.status(200).json(JSON.parse(cachedResponse));
}
// 実際のロジックを処理する(例:カードへの課金)
const result = { success: true, txnId: 'TXN-9982', total: req.body.amount };
// 結果を24時間(86400秒)キャッシュする
await redis.set(`idempotency:${idempotencyKey}`, JSON.stringify(result), 'EX', 86400);
res.status(201).json(result);
});
app.listen(3000, () => console.log('サーバーがポート3000で起動しました'));
堅牢なリクエストライフサイクルの仕組み
基本的な「チェックして設定する」アプローチは個人プロジェクトでは機能しますが、トラフィックの多い本番環境ではより高い回復力が必要です。私たちはクライアントとサーバー間の特定の契約、すなわち Idempotency-Key に依拠します。
クライアントの責任
フロントエンドやモバイルアプリは、新しいトランザクションごとに一意のV4 UUIDを生成する必要があります。ネットワーク障害などでクライアントが再試行する場合、*全く同じ* UUIDを送信します。サーバーはこの文字列をRedisの主要な検索キーとして使用します。
サーバーがリクエストを処理する方法
- 抽出: リクエストヘッダーから
Idempotency-Keyを取得します。 - 検証: すぐにRedisを確認します。結果が存在する場合は、ロジックをスキップしてキャッシュされたデータを返します。
- ロック: レースコンディションを防ぐために、キーを「進行中(In Progress)」としてマークします。
- 実行: Stripeの呼び出しやSQLテーブルの更新など、重い処理を実行します。
- 永続化: 最終的なJSON結果をRedisに保存します。
- クリーンアップ: Redisのメモリを節約するために、TTL(生存時間)を設定します。
これをミドルウェアとして実装することをお勧めします。このアプローチにより、コントローラーをビジネスロジックに集中させながら、わずか1行のコードですべてのPOSTまたはPUTルートを保護できます。
アトミック操作でレースコンディションを打破する
標準的なロジックには隠れた欠陥があります. 2つの同一のリクエストが5ミリ秒以内にAPIに到達した場合、両方が空のRedisキャッシュを確認し、二重請求をトリガーしてしまう可能性があります。これが典型的なレースコンディションです。
Redisを分散ロックとして使用する
これを解決するには、Redisの SET コマンドに NX(存在しない場合のみ設定)フラグを組み合わせて使用します。これにより、最初のリクエストが到着した瞬間にキーをアトミックにロックできます。
async function idempotencyMiddleware(req, res, next) {
const key = req.headers['idempotency-key'];
if (!key) return next();
const redisKey = `idempotency:${key}`;
// 60秒間のロック取得を試みる
const lockAcquired = await redis.set(redisKey, 'STARTED', 'NX', 'EX', 60);
if (!lockAcquired) {
const status = await redis.get(redisKey);
if (status === 'STARTED') {
return res.status(409).json({ error: '処理中です。しばらくお待ちください。' });
}
return res.status(200).json(JSON.parse(status));
}
// res.jsonをラップして、最終出力を自動的にキャッシュする
const originalJson = res.json;
res.json = (body) => {
redis.set(redisKey, JSON.stringify(body), 'EX', 86400);
return originalJson.call(res, body);
};
next();
}
並行リトライの処理
最初のリクエストがまだ処理されている間にリトライが到着した場合は、409 Conflict を返します。これにより、クライアントに少し待ってから再試行するように促します。これは、やみくもにロジックを2回実行して結果を運に任せるよりも、はるかにスマートな解決策です。
実行中にサーバーがクラッシュした場合はどうなるでしょうか? ‘STARTED’ 状態に設定された60秒のTTLにより、ロックは最終的に期限切れになります。これにより、システムの障害後でもクライアントが再試行して成功させることが可能になります。
戦略的なデプロイのヒント
コードは戦いの半分に過ぎません。APIが他の開発者にとっても予測可能であることを保証するために、確立された慣習に従う必要があります。
いつ冪等性を使用すべきか
非冪等なメソッドに注力してください。GETリクエストは設計上、冪等です。プロフィールページを100回更新しても、ユーザー名が変わることはありません。注文の作成、返金の実行、一括メールの送信など、副作用が発生する POST リクエストに焦点を当てましょう。
TTLの選択
これらの記録はどのくらいの期間保持すべきでしょうか?金融取引の場合、24時間から48時間が業界のスイートスポットです。この期間は、クライアントが接続不良から回復するには十分な長さであり、かつRedisインスタンスが肥大化するのを防ぐのに十分な短さです。数ヶ月間にわたって重複を追跡する必要がある場合は、最初の24時間後にRedisからPostgreSQLやMongoDBなどの永続ストレージにレコードを移行してください。
モニタリングは不可欠です。常に冪等性の「ヒット」をログに記録してください。重複キーの急増に気づいた場合、それは通常, クライアントのリトライロジックのバグや、インフラストラクチャの深刻なレイテンシの問題を示しています。これらのログは、システム全体の健康状態を知らせる「炭鉱のカナリア」として機能します。
これらのガードレールを構築するには事前の努力が必要ですが、それが脆弱なプロトタイプとプロフェッショナルな金融システムの違いとなります。ユーザーも、そしてあなたの睡眠時間も、それに感謝することになるでしょう。

