10日で覚えるSystem DesignDay 10: ECサイトとレートリミッターの設計

Day 10: ECサイトとレートリミッターの設計

今日学ぶこと

  • 商品カタログサービスの設計
  • ショッピングカートサービス
  • 注文・決済処理
  • 在庫管理(売り過ぎ防止)
  • Elasticsearchによる検索
  • 分散トランザクション(Sagaパターン)
  • レートリミッターのアルゴリズム詳解
  • 分散レートリミティング
  • 面接の最終アドバイス

Part 1: ECプラットフォーム

全体アーキテクチャ

flowchart TB
    Client["クライアント"]
    GW["API Gateway"]
    Client --> GW

    subgraph Services["マイクロサービス"]
        Product["商品サービス"]
        Cart["カートサービス"]
        Order["注文サービス"]
        Payment["決済サービス"]
        Inventory["在庫サービス"]
        Search["検索サービス"]
        Notify["通知サービス"]
    end

    GW --> Product
    GW --> Cart
    GW --> Order
    GW --> Search

    subgraph Data["データストア"]
        ProductDB["Product DB\n(PostgreSQL)"]
        CartDB["Cart\n(Redis)"]
        OrderDB["Order DB\n(PostgreSQL)"]
        InvDB["Inventory DB\n(PostgreSQL)"]
        ES["Elasticsearch"]
        MQ["Kafka"]
    end

    Product --> ProductDB
    Cart --> CartDB
    Order --> OrderDB
    Order --> Payment
    Order --> Inventory --> InvDB
    Product --> ES
    Search --> ES
    Order --> MQ --> Notify

    style Services fill:#3b82f6,color:#fff
    style Data fill:#8b5cf6,color:#fff

商品カタログサービス

-- Product catalog schema
CREATE TABLE products (
    id          UUID PRIMARY KEY,
    name        VARCHAR(255) NOT NULL,
    description TEXT,
    price       DECIMAL(10,2) NOT NULL,
    category_id UUID REFERENCES categories(id),
    seller_id   UUID NOT NULL,
    status      VARCHAR(20) DEFAULT 'active',
    created_at  TIMESTAMP DEFAULT NOW()
);

CREATE TABLE product_variants (
    id          UUID PRIMARY KEY,
    product_id  UUID REFERENCES products(id),
    sku         VARCHAR(100) UNIQUE NOT NULL,
    color       VARCHAR(50),
    size        VARCHAR(20),
    price       DECIMAL(10,2),
    image_urls  TEXT[]
);

検索にはElasticsearchを使用:

{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "ワイヤレスイヤホン" } }
      ],
      "filter": [
        { "range": { "price": { "gte": 3000, "lte": 10000 } } },
        { "term": { "category": "electronics" } }
      ]
    }
  },
  "sort": [
    { "_score": "desc" },
    { "sales_count": "desc" }
  ]
}

ショッピングカートサービス

カートはRedisに保存し、高速なアクセスを実現します。

flowchart LR
    subgraph CartFlow["カートのフロー"]
        Add["商品追加"]
        Update["数量変更"]
        Remove["商品削除"]
        Checkout["チェックアウト"]
    end
    subgraph Storage2["ストレージ"]
        Redis3["Redis\n(ログインユーザー)"]
        Cookie["Cookie/LocalStorage\n(ゲストユーザー)"]
    end
    CartFlow --> Storage2
    style CartFlow fill:#3b82f6,color:#fff
    style Storage2 fill:#f59e0b,color:#fff
// Redis data structure for cart
Key: cart:{user_id}
Value: Hash
  field: {product_variant_id}
  value: {quantity, added_at, price_at_add}

// TTL: 30 days (inactive carts expire)
ユーザー種別 カート保存先 特徴
ログインユーザー Redis(サーバー側) デバイス間で同期
ゲストユーザー Cookie / LocalStorage ログイン時にマージ

注文・決済処理

sequenceDiagram
    participant U as ユーザー
    participant O as 注文サービス
    participant I as 在庫サービス
    participant P as 決済サービス
    participant N as 通知サービス

    U->>O: 注文確定
    O->>I: 在庫確保(予約)
    I->>O: 確保成功
    O->>P: 決済処理
    alt 決済成功
        P->>O: 決済完了
        O->>I: 在庫確定
        O->>N: 注文確認メール送信
        O->>U: 注文完了
    else 決済失敗
        P->>O: 決済失敗
        O->>I: 在庫解放(ロールバック)
        O->>U: 注文失敗
    end

在庫管理 — 売り過ぎ防止

人気商品やフラッシュセールでは、同時に大量の注文が来ます。売り過ぎを防ぐ仕組みが重要です。

flowchart TB
    subgraph Problem["問題:売り過ぎ"]
        Stock["在庫: 1個"]
        R1["ユーザーA: 購入"]
        R2["ユーザーB: 購入"]
        R1 -->|"在庫チェック: 1 > 0 ✓"| Stock
        R2 -->|"在庫チェック: 1 > 0 ✓"| Stock
        Result["結果: 2人に販売(在庫は1個なのに!)"]
    end
    style Problem fill:#ef4444,color:#fff

解決策:

方式 仕組み メリット デメリット
悲観的ロック SELECT ... FOR UPDATE 確実 スループット低下
楽観的ロック バージョン番号チェック 高スループット リトライが必要
Redis DECR アトミックなデクリメント 超高速 Redis障害時のリスク
在庫予約パターン 仮確保 → 確定/解放 安全 複雑
// Redis-based inventory check (atomic operation)
WATCH inventory:{product_id}
val = GET inventory:{product_id}
if val > 0:
    MULTI
    DECR inventory:{product_id}
    EXEC
else:
    // Out of stock

分散トランザクション — Sagaパターン

マイクロサービスでは、複数のサービスにまたがるトランザクションにACIDは使えません。代わりにSagaパターンを使います。

flowchart LR
    subgraph Saga["Saga: 注文処理"]
        S1["1. 在庫確保"]
        S2["2. 決済処理"]
        S3["3. 配送手配"]
        S4["4. 通知送信"]
    end
    subgraph Compensate["補償トランザクション"]
        C1["在庫解放"]
        C2["返金処理"]
        C3["配送キャンセル"]
    end

    S1 -->|"成功"| S2
    S2 -->|"成功"| S3
    S3 -->|"成功"| S4

    S3 -.->|"失敗"| C2
    C2 -.->|"ロールバック"| C1

    style Saga fill:#22c55e,color:#fff
    style Compensate fill:#ef4444,color:#fff

Sagaの2つの実装方式:

方式 説明 メリット デメリット
Choreography 各サービスがイベントを発行・購読 疎結合 フローが追いにくい
Orchestration 中央のオーケストレーターが制御 フローが明確 オーケストレーターがSPOF
flowchart TB
    subgraph Orchestration["Orchestration方式"]
        Orch["Saga Orchestrator"]
        IS["在庫サービス"]
        PS["決済サービス"]
        SS["配送サービス"]
        Orch -->|"1. 在庫確保"| IS
        IS -->|"OK"| Orch
        Orch -->|"2. 決済"| PS
        PS -->|"OK"| Orch
        Orch -->|"3. 配送"| SS
    end
    style Orchestration fill:#3b82f6,color:#fff

Part 2: レートリミッター

なぜレートリミッターが必要か

  • DDoS攻撃の防御
  • APIの公平な利用の確保
  • コスト管理(クラウドリソースの過剰消費防止)
  • サービスの安定性確保

アルゴリズム詳解

1. Token Bucket

flowchart TB
    subgraph TokenBucket["Token Bucket"]
        Refill["トークン補充\n(10個/秒)"]
        Bucket2["バケット\n容量: 20個"]
        Refill -->|"一定レートで\n補充"| Bucket2
    end
    Req["リクエスト"] --> Bucket2
    Bucket2 -->|"トークンあり\n→ 消費"| Allow2["許可"]
    Bucket2 -->|"トークンなし"| Deny2["拒否 (429)"]

    style TokenBucket fill:#3b82f6,color:#fff
    style Allow2 fill:#22c55e,color:#fff
    style Deny2 fill:#ef4444,color:#fff
  • バースト対応: バケットが満タンなら一時的に多くのリクエストを許可
  • パラメータ: バケット容量、補充レート
  • 使用例: AWS API Gateway、Stripe

2. Leaky Bucket

flowchart TB
    subgraph LeakyBucket["Leaky Bucket"]
        Queue2["キュー\n(FIFO)"]
        Process["一定レートで処理\n(10個/秒)"]
    end
    Req2["リクエスト"] --> Queue2
    Queue2 -->|"キューに空きあり"| Process --> Allow3["処理"]
    Queue2 -->|"キューが満杯"| Deny3["拒否 (429)"]

    style LeakyBucket fill:#8b5cf6,color:#fff
    style Allow3 fill:#22c55e,color:#fff
    style Deny3 fill:#ef4444,color:#fff
  • 出力が均一: バーストを平滑化
  • パラメータ: キューサイズ、処理レート

3. Fixed Window Counter

Window: 1分間
Limit: 100リクエスト

[00:00 - 01:00] → カウント: 87  ✓
[01:00 - 02:00] → カウント: 103 ✗ (3件拒否)
  • 問題: ウィンドウ境界でバーストが発生する可能性
flowchart LR
    subgraph Boundary["境界問題"]
        W1["Window 1\n00:00-01:00\n最後に90件"]
        W2["Window 2\n01:00-02:00\n最初に90件"]
        W1 -->|"00:30-01:30の間に\n180件通過!"| W2
    end
    style Boundary fill:#ef4444,color:#fff

4. Sliding Window Log

各リクエストのタイムスタンプをログに記録し、ウィンドウ内のカウントを計算します。

Limit: 100/分
Window: 直近1分間

Request at 01:00:45 → Log: [00:59:50, 00:59:55, 01:00:10, ..., 01:00:45]
Count timestamps in [00:00:45, 01:00:45] = 98 → 許可 ✓
  • 正確: 境界問題なし
  • コスト: メモリ消費が大きい(全タイムスタンプを保存)

5. Sliding Window Counter

Fixed WindowとSliding Window Logの良いとこ取り。

Previous window count: 84
Current window count: 36
Current position in window: 25% elapsed

Weighted count = 84 × 0.75 + 36 = 99 → 許可 ✓

アルゴリズム比較

アルゴリズム メモリ効率 精度 バースト許容 実装の複雑さ
Token Bucket 良い 中 あり 低
Leaky Bucket 良い 中 なし(平滑化) 低
Fixed Window 良い 低 境界問題あり 低
Sliding Window Log 悪い 高 なし 中
Sliding Window Counter 良い 中〜高 少しあり 中

分散レートリミティング

複数のサーバーでレートリミットを共有する必要があります。

flowchart TB
    subgraph Clients["クライアント"]
        C1["Client A"]
        C2["Client B"]
    end

    subgraph LBs["Load Balancer"]
        LB2["LB"]
    end

    subgraph Servers["APIサーバー"]
        S1["Server 1"]
        S2["Server 2"]
        S3["Server 3"]
    end

    subgraph RateLimit["共有レートリミット"]
        Redis4["Redis Cluster\n(中央カウンター)"]
    end

    Clients --> LBs --> Servers
    S1 --> Redis4
    S2 --> Redis4
    S3 --> Redis4

    style Servers fill:#3b82f6,color:#fff
    style RateLimit fill:#ef4444,color:#fff

Redisでの実装:

// Sliding window counter with Redis
MULTI
  ZADD rate_limit:{client_id} {timestamp} {request_id}
  ZREMRANGEBYSCORE rate_limit:{client_id} 0 {timestamp - window_size}
  ZCARD rate_limit:{client_id}
EXEC
// If count > limit → reject (429)

レートリミティングの適用レイヤー:

レイヤー 実装場所 目的
L3/L4 ファイアウォール、iptables IP単位でDDoS防御
L7 API Gateway、Nginx ユーザー/API単位で制御
アプリケーション アプリ内ミドルウェア ビジネスロジック単位

面接の総まとめとアドバイス

システム設計面接のフレームワーク

flowchart LR
    subgraph Framework["面接の進め方 (40-45分)"]
        S1["1. 要件確認\n(5分)"]
        S2["2. 概算見積もり\n(5分)"]
        S3["3. ハイレベル設計\n(10分)"]
        S4["4. 詳細設計\n(15分)"]
        S5["5. スケーリング\n& まとめ\n(5-10分)"]
    end
    S1 --> S2 --> S3 --> S4 --> S5
    style Framework fill:#3b82f6,color:#fff

10日間の振り返り

Day テーマ キーワード
1 基礎概念 スケーラビリティ、レイテンシ、可用性
2 ネットワークとプロトコル TCP/UDP、HTTP、DNS、CDN
3 データベース設計 SQL/NoSQL、インデックス、シャーディング
4 キャッシュと整合性 Redis、Cache-Aside、CAP定理
5 分散システム レプリケーション、コンセンサス、メッセージキュー
6 マイクロサービスとAPI REST/gRPC、API Gateway、OAuth
7 URL短縮サービス Base62、キャッシュ戦略、リダイレクト
8 SNSフィードとチャット Fan-out、WebSocket、プレゼンス
9 動画配信とストレージ HLS、CDN、ファイルチャンキング
10 ECサイトとレートリミッター Saga、在庫管理、Token Bucket

面接で高評価を得るためのポイント

ポイント 説明
質問する 要件を自分から確認する。曖昧なまま設計を始めない
トレードオフを語る 「Aの方が良いがBのデメリットがある」と比較する
概算を示す QPS、ストレージ、帯域を数値で見積もる
ボトルネックを予測する 設計のどこが問題になるか自ら指摘する
段階的に設計する まず動くものを設計し、その後スケーリングを議論する
図を描く コンポーネント図、データフロー図を積極的に使う
非機能要件を意識する 可用性、レイテンシ、セキュリティに言及する

まとめ

今日のポイント一覧

トピック 重要ポイント
商品カタログ PostgreSQL + Elasticsearchで全文検索
ショッピングカート Redisに保存、ゲストはCookieで管理
注文処理 在庫確保 → 決済 → 確定の順序
在庫管理 Redis DECRまたは楽観的ロックで売り過ぎ防止
Sagaパターン 補償トランザクションで分散ロールバック
Token Bucket バースト許容、最も一般的なアルゴリズム
分散レートリミット Redisで中央カウンターを共有
レイヤー別制限 L3/L4(DDoS)、L7(API)、アプリ(ビジネスロジック)

練習問題

基礎レベル

  1. Token BucketとSliding Window Counterの違いを図を使って説明してください
  2. Sagaパターンの2つの実装方式(Choreography vs Orchestration)のメリット・デメリットを比較してください

中級レベル

  1. フラッシュセール(タイムセール)で1秒間に10万件の注文が来る場合の在庫管理システムを設計してください
  2. 分散レートリミッターをRedisで実装する場合、Redisがダウンした際の対策を3つ以上考えてください

チャレンジ

  1. チケット予約システム(Ticketmaster型)を設計してください。以下を含めてください:
    • 座席選択と仮予約(5分間のロック)
    • 決済処理
    • 売り切れ防止
    • 1人あたりの購入枚数制限
    • 販売開始時の大量アクセス対策(バーチャル待合室)
    • 1つのイベントに10万人が同時アクセスする場合のスケーラビリティ

参考リンク


おめでとうございます!

おめでとうございます!10日間のシステム設計の学習を完了しました。

この10日間で、システム設計面接に必要な基礎概念から実践的なシステム設計まで幅広く学びました。

これからの学習のために

  1. 繰り返し練習する: 学んだシステムを白紙から自分で設計し直してみましょう
  2. 実際のシステムを研究する: 有名企業のエンジニアリングブログを読みましょう
  3. 模擬面接をする: 友人やオンラインプラットフォームで練習しましょう
  4. 深掘りする: 興味のあるトピックをさらに深く学びましょう

最も大切なこと: システム設計に「唯一の正解」はありません。トレードオフを理解し、要件に基づいて合理的な判断ができることが、面接で最も評価されるポイントです。

あなたの面接の成功を応援しています!