冪等性・アトミック性・N+1問題など

Blog

実務で見落としがちだったAPI開発での注意点をまとめます。

知っているつもりでも、意外と考慮漏れが起きやすい点を整理するための学習記録です。


1. 冪等性について

PUT / DELETEなどは冪等であるべきとされています。
つまり、同じリクエストを複数回実行しても結果が変わらないことが求められます。

注意が必要なのは、「結果として同じ状態になるならUPDATEが実行されてもいい」という考え方は間違いである点です。

例として、差分がないのに毎回 updated_at が更新されてしまうと、それだけで冪等性が保てません。

対策

  • リクエスト内容と既存値を比較し、差分が無い場合は更新を行わない
  • PUTのロジック内で不要な副作用を発生させない

2. レースコンディション

複数の処理が同時に実行された場合に整合性が崩れる問題です。

例えば複数の購入リクエストがほぼ同時に来ると、どちらも「在庫あり」と判断して在庫数を減らし、結果的に在庫数がマイナスになることがあります。

対策

  • トランザクション
  • 悲観的ロック(SELECT ... FOR UPDATE
  • 楽観的ロック(versionカラム)
  • 必要に応じてキューで逐次処理にする

3. アトミック性

レースコンディションと関連はありますが、意味としては別のものです。
アトミック性は「処理がすべて成功するか、すべて失敗するか」を保証する性質です。

単一システム内ではトランザクションで実現できますが、複数のデータストアや外部APIが関わる場合は難しくなります。

対策

  • RDBではトランザクションで一貫性を担保
  • 別システムを跨ぐ場合は2フェーズコミットを検討

4. N+1問題

API層でも発生する典型的なパフォーマンス問題です。

ユーザー一覧を取得し、各ユーザーの追加情報を個別に取得するような処理を行うと、結果的に大量のクエリ / リクエストが発行されます。

対策

  • NoSQL:Batch取得や複数キーをまとめて問い合わせる設計
  • RDB:JOINやIN句、ORMのEager Loadingでまとめて取得
  • GraphQL:DataLoaderパターンというのがあるらしい

5. TTLつきの自前キャッシュ

外部サービスの状態確認や、ある程度変化しない値の取得など、毎回処理を行う必要が無いケースがあります。

規模が小さい場合は、専用キャッシュを導入せずに、時間制限(TTL)を付けた簡易キャッシュを実装するだけで十分なこともあります。

利用する場面

  • 外部サービスのヘルスチェック
  • 重い処理の結果を一定期間だけ保持
  • 設定値・ステータスの短期的なキャッシュ

実装例

let cacheValue = null;
let lastFetchedAt = null;
const TTL_MS = 60 * 1000; // 1分

async function getValue() {
  const now = Date.now();

  if (lastFetchedAt && now - lastFetchedAt < TTL_MS) {
    return cacheValue;
  }

  const fresh = await fetchOrCompute();
  cacheValue = fresh;
  lastFetchedAt = now;
  return fresh;
}

概念はともかく、アトミック性・レースコンディションなどの単語は最近知りました😶‍🌫️