はじめに
金融システムや決済APIとの連携において、ネットワークの瞬断やタイムアウトによるリトライは日常的に発生します。この際、同一の取引が重複して実行される「二重決済」を防ぐために不可欠なのが**冪等性(Idempotency)**の設計です。
本記事では、API連携における冪等性設計の基本方針と、Redisを用いた具体的な二重処理防止の実装例、および導入時のチェックリストについて解説します。
1. 冪等性設計の基本方針
APIにおける冪等性を担保する一般的なアプローチは、クライアントがリクエストごとに一意な識別子(Idempotency-Key)をヘッダーに付与し、サーバー側でそのキーの処理状態を管理する方法です。
処理フローの基本
-
キーの重複確認: 受信した
Idempotency-Keyがすでに処理中、または処理完了していないかを確認する。 - 分散ロックの取得: 同一キーに対する同時リクエスト(並行リクエスト)を防ぐため、ロックを取得する。
-
処理ステータスの管理:
- 未処理: 新規に処理を開始し、ステータスを「処理中(IN_PROGRESS)」にする。
- 処理中: 既に実行中のため、コンフリクトエラー(HTTP 409など)を返す。
- 処理完了: 過去に保存したレスポンスをそのまま返す。
2. Redisを用いた実装例 (TypeScript)
以下は、Node.js (TypeScript) と Redis を用いて、冪等キーの検証と二重送信防止を行うミドルウェアの実装例です。
import { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
interface IdempotencyRecord {
status: 'IN_PROGRESS' | 'COMPLETED';
response?: {
statusCode: number;
body: any;
};
}
export async function idempotencyMiddleware(req: Request, res: Response, next: NextFunction) {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey || typeof idempotencyKey !== 'string') {
return res.status(400).json({ error: 'Idempotency-Key header is required' });
}
const redisKey = `idempotency:${idempotencyKey}`;
try {
// 1. 既存の処理状態を確認
const existingRecordJson = await redis.get(redisKey);
if (existingRecordJson) {
const record: IdempotencyRecord = JSON.parse(existingRecordJson);
if (record.status === 'IN_PROGRESS') {
// 処理中の場合は409 Conflictを返す
return res.status(409).json({ error: 'Request is already in progress' });
}
if (record.status === 'COMPLETED' && record.response) {
// 処理完了済みの場合は、過去のレスポンスを返却
return res.status(record.response.statusCode).json(record.response.body);
}
}
// 2. 処理中ステータスをセット(アトミックにロックを取得、有効期限は10分に設定)
const recordInProgress: IdempotencyRecord = { status: 'IN_PROGRESS' };
const acquired = await redis.set(
redisKey,
JSON.stringify(recordInProgress),
'NX',
'EX',
600
);
if (!acquired) {
return res.status(409).json({ error: 'Request is already in progress or processed' });
}
// レスポンスをインターセプトして結果をRedisに保存する仕組み
const originalJson = res.json;
res.json = (body) => {
const responseRecord: IdempotencyRecord = {
status: 'COMPLETED',
response: {
statusCode: res.statusCode,
body,
},
};
// 処理完了後に結果を保存(有効期限は24時間など要件に応じて調整)
redis.set(redisKey, JSON.stringify(responseRecord), 'EX', 86400).catch(console.error);
return originalJson.call(res, body);
};
next();
} catch (error) {
console.error('Idempotency middleware error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
3. 状態遷移とエラーハンドリングの比較
リクエストの状況に応じて、APIが返すべきステータスコードと対応方針を整理します。
| クライアントの状態 | サーバー側の状態 | 推奨されるHTTPステータス | クライアント側の対応 |
|---|---|---|---|
| 初回リクエスト | 未処理 | 200 OK / 201 Created | 通常処理 |
| 同時並行リクエスト | 処理中 (IN_PROGRESS) | 409 Conflict | 一定時間待機した後に再試行 |
| リトライ(成功後) | 完了 (COMPLETED) | 200 OK (キャッシュ返却) | 成功として処理を継続 |
| リトライ(システムエラー後) | 失敗 / レコードなし | 500 Internal Server Error | 新しいキー、または同一キーで再試行(設計による) |
4. 導入時の注意点とチェックリスト
金融システムで冪等性を導入するにあたり、設計段階で考慮すべきポイントです。
- キーの有効期限(TTL)の設定: 冪等キーの保存期間は、業務要件(例:決済の再試行可能期間)に合わせて設定されているか(例:24時間〜数日間)。
-
リクエストボディのハッシュ検証: 同一の
Idempotency-Keyで異なるリクエストボディが送信された場合、不正なリクエストとしてエラー(400 Bad Request)を返しているか。 - Redisの可用性: Redisが単一障害点(SPOF)にならないよう、Redis Clusterやレプリケーション構成が組まれているか。
- データベースのトランザクションとの整合性: Redisへのステータス保存と、メインデータベース(RDB)のコミットタイミングに不整合が生じない設計になっているか。
まとめ
API連携における二重処理の防止は、システムの信頼性を担保する上で極めて重要です。Idempotency-Key を用いた分散ロックと状態管理を適切に実装することで、ネットワーク不安定化に伴うリトライ処理を安全に受け入れることが可能になります。
株式会社ブリンクグループでは、エンジニアの業務やキャリアに役立つ情報を発信しています。フリーランス案件をお探しの方は、E-Bridgeもご覧ください。
https://ebridge.jp/