古い価格や在庫を表示しないECサイトのキャッシュ戦略

March 8, 2026

キャッシュはECサイトのパフォーマンスを向上させる最も効果的な手段のひとつです。しかし同時に、顧客の信頼を損なう最も手軽な方法でもあります。カートに¥1,290で追加した商品が、チェックアウト時に¥1,790で請求されたとしたら——その顧客が再び戻ってくる可能性は極めて低いでしょう。あるいは、3時間前に売り切れた商品に「カートに追加」ボタンが表示されたままになっていれば、問い合わせが殺到することになります。

このガイドでは、正しいデータを正しいTTLでキャッシュし、適切な無効化戦略を組み合わせることで、速度と正確性を両立する方法を解説します。

日本市場の特性について: Amazon.co.jp、楽天市場、Yahoo!ショッピング、au PAYマーケット、ZOZOTOWNといった主要プラットフォームでは、タイムセールや超PayPay祭、楽天スーパーSALEなどの大型キャンペーン時に価格変動と同時アクセスが急増します。また、消費税の表示ルール(税込価格の明示義務)や、各モールのポイント倍率変更なども価格データの鮮度に影響します。キャッシュ設計はこうした日本市場固有の要件を踏まえて行う必要があります。


なぜECサイトのキャッシュは特別なのか

一般的なキャッシュのガイドは、データの古さ(Staleness)をある程度許容できるトレードオフとして扱います。しかしECサイトでは、絶対に古くなってはいけないデータがあります:

  • 価格 — セール・クーポン・消費税率・ポイント倍率の変更により随時更新される
  • 在庫数 — 限定品やタイムセール時は秒単位で変動する
  • 割引コード — セッション中に有効期限切れや利用上限に達する可能性がある
  • 送料 — 配送業者(ヤマト運輸、佐川急便、日本郵便など)の料金体系や届け先によって変わる

一方、ある程度の古さを許容できるデータもあります:

  • 商品画像・商品説明文
  • カテゴリ階層・ナビゲーション構造
  • レビュー・評価
  • レコメンド商品

重要なのはデータの種類ごとに適切なグループを判断し、それぞれ異なるキャッシュ戦略を適用することです。


ステップ1:データを鮮度許容度で分類する

キャッシュロジックを書く前に、ストアのデータ分類テーブルを作成してください。以下はスターターテンプレートです:

データ種別 鮮度許容度 推奨TTL 無効化トリガー
商品画像 高 24時間〜7日 アセット再公開時
商品説明文 中 1〜4時間 コンテンツ更新時
カテゴリ・ナビゲーション 中 1〜2時間 カタログ構造変更時
通常価格 低 5〜15分 価格ルール変更時
セール価格 非常に低 1〜2分 キャンペーン開始・終了時
在庫数 非常に低 30〜60秒 注文発生・在庫更新時
割引コードの有効性 不可 キャッシュしない 常にライブチェック
カート合計金額 不可 キャッシュしない 常にライブ計算

基本原則: 古いデータが金額の不一致や顧客への誤った約束につながる場合は、キャッシュしない——あるいは即時無効化を伴うWrite-through戦略を採用してください。


ステップ2:多層キャッシュアーキテクチャを使用する

単一のキャッシュ層は単一障害点となり、すべてのデータに同じTTLを適用することになります。代わりに、役割の異なる3層構造を使いましょう。

リクエスト
  │
  ▼
┌─────────────────────────┐
│   CDN / エッジキャッシュ  │  ← 静的アセット、レンダリング済みカテゴリページ
└─────────────────────────┘
  │ キャッシュミス
  ▼
┌─────────────────────────┐
│  アプリケーションキャッシュ │  ← 商品データ、価格リスト、在庫スナップショット
│  (Redis / Memcached)    │
└─────────────────────────┘
  │ キャッシュミス
  ▼
┌─────────────────────────┐
│   オリジン / データベース  │  ← 信頼できる唯一の情報源:実際の価格・在庫
└─────────────────────────┘

第1層:CDN / エッジキャッシュ

パーソナライゼーションなしのカテゴリページ・商品ページのレンダリング済みHTMLをキャッシュします。Surrogate Keys(Cloudflare、Fastly、AWS CloudFrontで対応)を使用し、商品変更時にその商品を参照するすべてのページを一括無効化できるようにします。

CDNレベルでキャッシュしてはいけないもの:

  • ログイン済みユーザーの価格を表示するページ(法人向け特別価格、会員ランク別価格など)
  • カートの状態を反映するページ
  • リアルタイム在庫を表示するページ(「残り2点!」など)

Vary: Cookie ヘッダーの設定ミスに注意してください。誤った設定では、ログイン済みユーザー全員のCDNキャッシュが意図せずバイパスされてしまいます。

第2層:アプリケーションキャッシュ(Redis)

最も重要なワークホース層です。解決済みの価格リスト、在庫スナップショット、商品属性セット、複数テーブルのJOINを必要とするデータをキャッシュします。

# 例:短いTTLとCache-Asideパターンによる価格取得
def get_price(product_id: str, customer_group: str) -> Decimal:
    cache_key = f"price:{product_id}:{customer_group}"
    cached = redis.get(cache_key)

    if cached:
        return Decimal(cached)

    price = db.query_price(product_id, customer_group)
    redis.setex(cache_key, ttl=60, value=str(price))  # TTL: 60秒
    return price

名前空間付きキー(price:、stock:、product:)を使用することで、一括更新時に関連するキャッシュカテゴリだけをフラッシュし、他のキャッシュエントリに影響を与えずに済みます。

第3層:オリジン / データベース

この層でのキャッシュは行いません——ここが唯一の正確な情報源です。この層でのキャッシュはアプリケーションロジックではなく、データベース自身のクエリキャッシュやリードレプリカに任せてください。


ステップ3:イベント駆動型のキャッシュ無効化を実装する

TTLによる自動期限切れはあくまでセーフティネットであり、戦略ではありません。価格と在庫データにはイベント駆動型の無効化が必要です——データソースが変更された瞬間にキャッシュをクリアします。

パターン:書き込み時に発行、消費時に無効化

ERPやバックオフィスシステムで価格更新
        │
        ▼
  メッセージブローカー
  (Kafka / AWS SQS / Redis Pub/Sub)
        │
        ▼
  キャッシュ無効化サービス
        │
        ├── Redisキーを削除: price:{product_id}:*
        └── CDN Surrogate Keyをパージ: product-{product_id}

このアプローチにより、TTL設定に関わらず、価格変更がコミットされてから数秒以内にキャッシュが更新されます。

イベントに含めるべき情報

価格・在庫更新イベントには、正確な無効化に必要なコンテキストを含めてください:

{
  "event": "price_updated",
  "product_id": "SKU-12345",
  "affected_customer_groups": ["general", "premium_member"],
  "effective_at": "2026-03-08T10:00:00+09:00"
}

正確なイベント = 正確な無効化。トラフィックのピーク時に「全キャッシュ削除」を行うことは避けてください——すべてのキャッシュミスが同時にデータベースへ集中するThunderding Herd問題を引き起こします。


ステップ4:キャッシュミス時のThundering Herdを防ぐ

人気キャッシュキーが期限切れになると、数百のリクエストが同時にデータベースにアクセスする可能性があります。これがThundering Herd問題で、タイムセール中にシステムをダウンさせる原因になります。

方法A:確率的早期失効(TTLジッター)

TTLにランダムなジッターを加えることで、類似商品のキャッシュが一斉に期限切れになるのを防ぎます:

import random

BASE_TTL = 60  # 秒
jitter = random.randint(0, 10)
redis.setex(cache_key, ttl=BASE_TTL + jitter, value=data)

方法B:リクエストコアレッシング(Single-Flight)

キャッシュミス時に1つのリクエストだけが再計算を行い、他は待機するようにします:

# Distributed Lockを使って同時再計算を防ぐ
lock_key = f"lock:{cache_key}"

if redis.set(lock_key, "1", nx=True, ex=5):  # nx = キーが存在しない場合のみセット
    # このリクエストがロックを取得 — 再計算してキャッシュを更新
    value = db.fetch(...)
    redis.setex(cache_key, ttl=60, value=value)
    redis.delete(lock_key)
else:
    # 別のリクエストが再計算中 — 少し待ってリトライ
    time.sleep(0.05)
    value = redis.get(cache_key)

方法C:Stale-While-Revalidate

バックグラウンドで非同期に再計算しながら、古いキャッシュ値をすぐに返します。商品説明やナビゲーションには適していますが、正確性が求められる価格には使用しないでください。


ステップ5:チェックアウトは常にライブデータを使用する

商品ページのキャッシュがどれほど優れていても、チェックアウトフローでは価格と在庫のすべての計算にライブデータを使用しなければなりません。

チェックアウト時の必須ライブチェック:

  1. チェックアウト開始時にカート金額を再計算 — 現在の価格を取得し、プロモーションを再適用し、消費税10%(軽減税率8%が適用される場合はその旨も)を再計算する
  2. 在庫の確保は注文確認時に行う(「カートに追加」時ではない)— 短い確保時間(例:15分)を設け、購入完了しない場合は解放する
  3. 割引コードをライブで検証する — 使用時点で有効期限・利用上限・利用条件をリアルタイムで確認する
  4. 最終ステップで送料を再確認する — セッション中に配送業者(ヤマト運輸・佐川急便・日本郵便)の料金やエリア区分が変わる場合がある

在庫確保の一般的なパターン:

顧客が「注文する」をクリック
        │
        ▼
  在庫確保を試みる(下限チェック付きでデクリメント)
        │
  ┌─────┴──────┐
  │ 成功        │ 失敗(在庫 = 0)
  │            │
  ▼            ▼
決済処理へ    「申し訳ございません、
              この商品は在庫切れです」を返す
              (決済前に通知)

在庫の確認が取れていない商品に対して、決済処理を試みてはいけません。


ステップ6:本番環境でキャッシュの健全性を監視する

キャッシュの問題はサイレント障害です。顧客が気づく前に検知できるよう、監視体制を整えましょう。

追跡すべきメトリクス:

メトリクス 意味 アラート閾値
キャッシュヒット率 キャッシュ全体の有効性 80%未満は要調査
古い読み取り率 TTL期限切れデータの配信頻度 トレンド監視、急増時にアラート
無効化ラグ データ変更からキャッシュクリアまでの時間 価格データで30秒超は問題
キャッシュ退避率 キャッシュが小さすぎるかキーが大きすぎる 退避率高 = サイズ拡張またはキー整理
価格不整合イベント カート金額 ≠ 注文確定金額 1件でも発生したら要調査

カナリアチェックの設定: 60秒ごとに既知の商品の価格をキャッシュとデータベースの両方から取得し、許容範囲を超えた差異があればアラートを発報します。顧客が不整合に気づく前に早期警告を受け取れます。


ステップ7:セールやピーク時は特別な対応を行う

タイムセールは通常のキャッシュの前提を崩します。価格は時間とともに変わり、在庫は数秒でなくなり、トラフィックは10〜100倍に急増します。

日本市場特有の注意事項: 楽天スーパーSALE、超PayPay祭、Amazonプライムデー、ZOZOのセール、各モールの年末年始セールや母の日・父の日セールなど、集中的なトラフィック増加が予測されるイベントは事前に把握してシステムを準備することが重要です。また、午前0時や午前10時など特定の時刻にセールが一斉スタートするケースでは、その瞬間に負荷が集中します。

イベント開始前にキャッシュをウォームアップする——Goライブの数分前にRedisへ価格・商品データを投入しておくことで、最初のトラフィック波がコールドキャッシュに当たるのを防ぎます。

高頻度の在庫更新には専用の在庫サービスを使用する。Redisカウンターによるアトミックデクリメント(DECR)は、同時負荷下でデータベースの行ロックよりはるかに高いパフォーマンスを発揮します:

# アトミックな在庫デクリメント — デクリメント後の残在庫を返す
remaining = redis.decr(f"stock:{product_id}")

if remaining < 0:
    # 在庫オーバーセル — 元に戻して拒否
    redis.incr(f"stock:{product_id}")
    raise OutOfStockError()

負荷下でのグレースフルデグラデーション: キャッシュ層が利用不能になった場合、フォールバックはキャッシュなしのリクエストをデータベースに集中させるのではなく、シンプルなメッセージ(「チェックアウトで在庫をご確認ください」)を返すようにします。


本番リリース前チェックリスト

  • [ ] データ分類テーブル完成 — すべてのデータ種別にTTLと無効化戦略が割り当てられている
  • [ ] 3層キャッシュアーキテクチャが整備されている(CDN + アプリケーションキャッシュ + オリジン)
  • [ ] 名前空間付きRedisキーによるターゲット無効化が実装されている
  • [ ] 価格・在庫変更にイベント駆動型無効化が対応している
  • [ ] Thundering Herd防止のためのTTLジッターが適用されている
  • [ ] チェックアウトパスがライブデータのみを使用していることを確認済み
  • [ ] 注文確認時の在庫確保ロジックが実装されている
  • [ ] 割引コードの検証は常にライブで行われている
  • [ ] キャッシュヒット率・無効化ラグ・価格不整合の監視が稼働している
  • [ ] タイムセール用のRunbookが作成・テスト済みである

まとめ

高速かつ正確なECサイトのキャッシュは、どちらかを選ぶ問題ではありません——正しいデータに正しい戦略を適用することです。静的コンテンツは積極的にキャッシュする。価格と在庫には短いTTLとイベント駆動型無効化を適用する。カート合計や割引コードの有効性は絶対にキャッシュしない。そして、チェックアウトパスではライブデータを絶対的な前提として構築する。

これを正しく実現したチームは、単に速いストアを持つだけでなく——顧客が信頼できるストアを手に入れます。


ECサイトのキャッシュアーキテクチャのレビューをご希望ですか?Simplicoへの無料相談はこちら

Ready to talk about your project?

Share goals and constraints. We'll assemble architects and engineers to move fast with you.

Get in touch