TECH JOURNAL

Amazon SP-API 注文同期を冪等に設計する — 重複処理・欠損・ロールバックの全対策

ShareXB!
Amazon SP-API 注文同期を冪等に設計する — 重複処理・欠損・ロールバックの全対策
目次

はじめに

Amazon SP-API で getOrders / getOrderItems を定期ポーリングして社内 OMS に取り込む処理は、一見シンプルに見える。しかし本番運用に入ると、以下の問題が次々と現れる。

  • Lambda が途中でタイムアウトし、一部注文だけ登録された
  • 同一の AmazonOrderId が 2 回 OMS に入り、2 重出荷になった
  • Amazon 側の遅延で LastUpdatedAfter ウィンドウをまたいだ注文が欠損した

これらをまとめて解決するのが 冪等設計 だ。

冪等性の原則

同じ入力を何度処理しても、最終状態が 1 回実行した結果と変わらない — これが冪等性の定義。注文同期で言えば「同じ AmazonOrderId を 100 回処理しても、OMS の注文レコードは 1 件だけ存在する」状態を保証する。

設計の全体像

SP-API getOrders
    ↓
SQS に AmazonOrderId を Enqueue(重複排除は SQS MessageDeduplicationId で)
    ↓
Lambda Worker: 1 メッセージ = 1 注文を処理
    ↓
DynamoDB: 条件付き Put("item NOT EXISTS" で重複防止)
    ↓
OMS へ書き込み(外部 API は Client-side idempotency key 付与)

実装パターン 1: SQS MessageDeduplicationId

FIFO キューの MessageDeduplicationId に AmazonOrderId を使うだけで、5 分以内の重複メッセージを SQS 側で排除できる。

await sqs.sendMessage({
  QueueUrl: QUEUE_URL,
  MessageGroupId: "orders",
  MessageDeduplicationId: order.AmazonOrderId, // FIFO キューの重複排除キー
  MessageBody: JSON.stringify({ orderId: order.AmazonOrderId }),
})

実装パターン 2: DynamoDB 条件付き Put

5 分の SQS ウィンドウを超えた重複(リドライブやリプレイ)は DynamoDB 側で防ぐ。attribute_not_exists(pk) を条件に指定し、すでに存在するレコードへの上書きを拒否する。

await dynamodb.put({
  TableName: "orders",
  Item: { pk: orderId, ...orderData },
  ConditionExpression: "attribute_not_exists(pk)",
}).catch((err) => {
  if (err.name === "ConditionalCheckFailedException") {
    // 既処理 → 正常終了扱い
    return
  }
  throw err
})

ConditionalCheckFailedException は「すでに処理済み」を意味するので、エラーとして再スローしてはいけない。

実装パターン 3: LastUpdatedAfter ウィンドウの重複許容

getOrders は LastUpdatedAfter でフィルタするが、境界付近の注文は前回と今回の両ウィンドウに含まれる。これは SP-API の仕様なので、ウィンドウを 10 分重複させて取得し、OMS 側の冪等 upsert に任せる。

const lastUpdatedAfter = new Date(
  lastRunTime.getTime() - 10 * 60 * 1000 // 10 分のオーバーラップ
).toISOString()

実装パターン 4: 外部 API への冪等キー付与

OMS が REST API の場合、リクエストごとに Idempotency-Key ヘッダを付与する。Lambda がタイムアウト後にリトライしても、同一キーなら OMS は同じレスポンスを返す。

const idempotencyKey = `sp-order-${orderId}`
await omsClient.post("/orders", payload, {
  headers: { "Idempotency-Key": idempotencyKey },
})

冪等キーは リトライをまたいで同じ値 になるよう、orderId など決定論的な値だけで構成する。Date.now() や UUID をキーに含めると、Lambda タイムアウト後のリトライ時に別のキーになり、OMS が別注文として扱ってしまう。

欠損対策: Checkpoint + Full Scan の組み合わせ

差分ポーリングだけに頼ると、Amazon 側の遅延やシステム障害で注文が欠損することがある。週次で CreatedAfter=7日前 のフルスキャンを走らせ、OMS に未登録の AmazonOrderId をバックフィルするジョブを入れておくと保険になる。

// 週次バックフィル: 差分ポーリングでこぼれた注文を拾う
async function weeklyBackfill() {
  const createdAfter = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
  for await (const order of paginate(ordersApi, { createdAfter })) {
    const exists = await checkOmsExists(order.AmazonOrderId)
    if (!exists) await enqueue(order.AmazonOrderId)
  }
}

まとめ

問題対策
Lambda タイムアウト後の重複SQS FIFO + DynamoDB 条件付き Put
ウィンドウ境界の欠損10 分オーバーラップ
OMS への二重書き込みIdempotency-Key ヘッダ
差分ポーリングの欠損週次フルスキャン バックフィル
ShareXB!

この記事を書いた人

渡部 誠也

執行役員 / CTO

独立系 SIer で Web・組み込み・基幹システムの開発を経験し、2017 年に illustrious へ。CTO としてシステム開発事業を立ち上げ、要件定義からコーディングまで一貫して担う。EC に特化した Web アプリケーションを数多く手がける。

ECの業務やシステムについて、
ご相談ください。

いまの運用で困っていること、実現したいことから、一緒に整理します。