REST APIにおける冪等性(Idempotency):Node.jsとRedisで「二重決済」を防止する究極のテクニック

Development tutorial - IT technology blog
Development tutorial - IT technology blog

なぜ冪等性(Idempotency)がこれほど重要なのか?

顧客の立場になって考えてみてください。1万円の注文のために「支払う」ボタンを押しました。しかし、ネットワークが不安定で、読み込み中のアイコンが回り続けています。焦ったあなたはボタンをさらに3回クリックしました。通知が届いたときには、口座から4万円が引き落されていました。これはユーザーのミスではなく、バックエンドエンジニアによる深刻な設計ミスです。

大手決済システムのデータによると、クライアントやネットワークによるリトライに起因する重複リクエストの割合は、全トラフィックの0.5%〜2%に達することがあります。適切な制御メカニズムがなければ、データベースはすぐに整合性を失い、混乱に陥るでしょう。

私が以前Fintechのスタートアップで働いていた際、ネットワークの輻輳によって数百件の重複取引が記録されるという障害が発生しました。手動でデータをロールバックするために一晩中作業する羽目になりました。その衝撃から、冪等性(Idempotency)(繰り返し実行しても結果が変わらない性質)は、機密性の高いすべてのAPIにとって必須の要素であると痛感しました。

HTTPメソッドの本来の性質

すべてのメソッドで冪等性の処理が必要なわけではありません。それぞれの設計を見てみましょう:

  • GET, HEAD: 本質的に読み取り専用です。1回呼び出しても1,000回呼び出しても、サーバー上のデータは変わりません。
  • PUT: 上書きのために使用されます。同じペイロードを複数回送信しても、最終的な状態は同じです。
  • DELETE: リソースを削除します。削除された後は、その後の呼び出しは単にリソースが存在しないことを確認するだけです。
  • POST: これがすべてのトラブルの元です。POSTを呼び出すたびに、通常は新しいレコード(注文、取引など)が作成されます。このメソッドに対して重点的に対策を講じる必要があります。

Idempotency Key(冪等性キー)戦略

最も最適な解決策は、リクエストのヘッダーにIdempotency Keyを付与して送信することです。標準的なプロセスは以下の通りです:

  1. クライアントは、各アクションに対して一意のUUIDを生成し、ヘッダー x-idempotency-key で送信します。
  2. サーバーはリクエストを受け取り、Redis内でこのキーを確認します。
  3. キーが見つかった場合:サーバーはビジネスロジックを再実行することなく、以前に保存された結果を即座に返します。
  4. 完全に新しいキーの場合:サーバーは処理を実行し、結果をTTL(有効期限)付きでRedisに保存してから、クライアントにレスポンスを返します。

実践:Node.jsとRedisによるミドルウェアの構築

Express.jsとRedisを組み合わせて、APIのための強固な保護層を作成します。

1. 環境のセットアップ

開始するために必要なライブラリをインストールします:

npm install express redis uuid

2. Redis接続の初期化

Redisは、アクセスの速さ(1ms未満)と、一定時間後にキーを自動削除できる機能(TTL)を備えているため、最適な選択肢です。

const express = require('express');
const redis = require('redis');
const { v4: uuidv4 } = require('uuid');

const app = express();
app.use(express.json());

const redisClient = redis.createClient();
redisClient.connect().then(() => console.log('✅ Redisの準備が完了しました'));

const CACHE_TTL = 3600; // 60分間結果をキャッシュする

3. インテリジェントな処理を行うミドルウェアの作成

このミドルウェアはゲートキーパーの役割を果たし、重複リクエストがコントローラーに到達する前に阻止します。

const idempotencyMiddleware = async (req, res, next) => {
  const key = req.headers['x-idempotency-key'];
  if (!key) return next();

  try {
    const cached = await redisClient.get(`idempotency:${key}`);
    if (cached) {
      const { status, body } = JSON.parse(cached);
      return res.status(status).json(body);
    }

    // res.sendをオーバーライドしてレスポンス結果をキャプチャする
    const originalSend = res.send;
    res.send = function (body) {
      if (res.statusCode >= 200 && res.statusCode < 300) {
        redisClient.setEx(`idempotency:${key}`, CACHE_TTL, 
          JSON.stringify({ status: res.statusCode, body: JSON.parse(body) })
        );
      }
      return originalSend.call(this, body);
    };
    next();
  } catch (err) {
    next(err);
  }
};

本番環境への導入における3つの重要な注意点

コードをすぐにプロダクション環境にコピーする前に、以下の3つの問題に対処する必要があります:

レースコンディションの回避 1msの間に2つのリクエストが同時に届いた場合、Redisが最初のキーを保存する時間が足りない可能性があります。リクエストを受け取った直後に SET NX コマンドを使用して、一時的なロックを作成してください。

キーのスコープ: クライアントからのUUIDを完全に信頼しないでください。異なるアカウント間でのキーの衝突を避けるために、ユーザーIDと組み合わせてください(例:idempotency:user_99:key_abc)。

成功時のみキャッシュする: 500エラーや503エラーをキャッシュしてはいけません。処理中にシステムがダウンした場合は、サーバーが復旧した後にクライアントが再試行できるようにする必要があります。

結論

冪等性の実装は単なる技術的な問題ではなく、顧客のデータに対する責任の問題です。Node.jsとRedisを使用すれば、わずか数行のコードで非常に効果的な保護層を構築できます。決済や注文のAPIにすぐに適用して、システムのプロフェッショナル性と信頼性を高めましょう。

Share: