Amazon 價格追蹤服務 (Design Amazon Price Tracking Service)

一句話定位 + TL;DR(含 35 分鐘心智時鐘)

設計一個 CamelCamelCamel 式 的服務:追蹤 5 億個 Amazon 商品價格歷史(畫圖表),並讓使用者 訂閱價格下跌通知(設門檻、跌破時 1 小時內 email)。系統明確 AP 取向(可用性優先、可接受 eventual consistency),價格歷史查詢要 < 500ms
招牌取捨:資料蒐集 不靠暴力爬遍 5 億頁(單算就要 >5 天、資料早就 stale)——改用 browser extension 群眾外包 + 優先式 crawler 的 hybrid:使用者在逛 Amazon 時 extension 順手回報價格,天然優先涵蓋熱門 / 趨勢商品,crawler 只補沒人看的長尾。代價是 覆蓋不均勻 + 要防惡意回報(先驗證完整性,可疑回報觸發高優先重爬)。其餘決勝點:通知從 cron 拉取改成 CDC / dual-write 的 event-driven 推送價格歷史靠每日 pre-aggregation 表(非掃 raw)滿足 < 500msappend-only 時間序列存 DynamoDB/Cassandra(product_id 分區、timestamp 排序)
配速(35 min 預算):~5 min 釐清需求 → ~4 min 容量估算 → ~3 min API + ~4 min 資料模型 → ~5 min 高階架構 → ~9 min deep dive(crawler 蒐集 / event-driven 通知 / 歷史查詢加速)→ ~3 min 擴展瓶頸 → ~2 min 收尾。開場先一句話複述題目、宣告配速、把三個 deep dive 標記為「待會展開」——宣告配速本身就是高分動作,替你爭取「先框架、後細節」的許可。

1. 釐清需求與範圍 (Clarify & Scope ~5 min)

開場後先框邊界,別急著畫架構。本題的關鍵釐清是「我們爬不爬得動 Amazon」與「通知要多即時」。

釐清問題 為何要問 / 影響到哪個決策 本題假設(依來源)
要追蹤多少商品? 直接決定爬取策略是否可行 5 億(500M)個 Amazon 商品,每天新增 ~3000 個
入口有哪些? 決定 client 端、是否能借力使用者 網站 + Chrome extension
通知要多即時? 決定 pull(cron) vs push(event) 價格變動後 1 小時內 送出
歷史查詢延遲要求? 決定要不要 pre-aggregate < 500ms
一致性 vs 可用性? 決定 DB 選型與容錯策略 可用性優先、可接受 eventual consistency(AP)
通知門檻是使用者自訂嗎? 決定 Subscription schema 與查詢 ,使用者設定 price_threshold
我們自己掌控 Amazon 嗎? 決定資料來源是 crawl 還是 API ,只能對外部站爬取(受其 rate limit 約束)
defer 話術

怎麼有效率地爬 5 億個商品 是這題真正的硬核,我先標記成 deep dive;現在先把 crawler 當成一個 會定期把價格寫進 DB 的 black box,這樣我們能先把『查歷史』與『訂閱通知』兩條主 flow 串起來。」把最難的推遲,保住敘事連貫。

2. 功能性與非功能性需求 (FR / NFR 與設計北極星)

設計北極星(NFR 直接導出後續所有選型的判準):

NFR 導出的設計北極星 後續對映選型
5 億商品 + 受 rate limit 不能暴力爬;要優先化 + 借力使用者 hybrid(extension 群眾外包 + 優先式 crawler)
歷史查詢 < 500ms 讀路徑不可掃 raw time-series;要預先彙總 每日 pre-aggregation 表 + index;或 TSDB/OLAP
通知 1 小時內 不能靠低頻 cron 拉取;要 event-driven 推送 CDC / dual-write → message queue → worker
AP / eventual consistency 寫高吞吐、可放鬆強一致;append-only DynamoDB / Cassandra(time-series 存取模式)
一句話北極星

「這是一個 寫多(價格不斷灌入)、讀要快(圖表 < 500ms)、通知要近即時(< 1h)、且資料來源受制於外部 的系統」——後面每個選型都能從這四點推回去。

3. 容量估算 (Back-of-Envelope ~4 min)

估算目的是 印證『暴力爬不可行』與『raw 掃描撐不住延遲』 這兩個驅動全題的結論。回連 02-Distributed-Systems/05-Numbers-to-Know

暴力爬一輪要多久(驅動 hybrid 決策)

Amazon 對單一 IP rate limit ≈ 1 visit / sec
即使用 1000 個不同 IP → 1000 visits / sec
掃完 5 億頁 = 5e8 ÷ 1000 ÷ 86,400 s ≈ 5.78 天
→ 一輪 >5 天 → 資料嚴重 stale → 暴力爬不可行(關鍵結論)

單一熱門商品的 raw 資料量(驅動 pre-aggregation 決策)

熱門商品每小時更新一次 → 1 年 8,760 筆 → 2 年 ≈ 17,520 筆 / 商品
畫一張 2 年圖表若掃 raw:單商品就要掃 ~17,520 筆並 GROUP BY
× 數百萬使用者同時看不同商品 → DB 上跑 analytics query 撐不住 < 500ms
→ 必須預先彙總(關鍵結論)

下列數字皆為 推導值:來源僅給出 5 億商品、1 IP=1visit/s、熱門商品每小時更新、2 年區間等輸入;天數與筆數由此算出。

維度 數字 對後續決策的意義
暴力爬一輪 > 5.78 天(推導值,1000 IP) 暴力爬會 stale → 走 hybrid 優先式 + extension
2 年 raw / 熱門商品 ≈ 17,520 筆(推導值) 掃 raw 畫圖撐不住 500ms → pre-aggregation
新商品成長 ~3000 / day 需靠 extension 回報 + crawler 共同 發現新商品
寫吞吐 高且 append-only DynamoDB / Cassandra(time-series 存取)
訂閱寫吞吐 低很多(僅來自使用者操作) 訂閱獨立成 Subscription DB,不與價格寫混在一起
估算的最大收穫

兩個數字(>5 天 / 17,520 筆)分別釘死了本題兩個招牌決策:不暴力爬不掃 raw。把這兩個算式說清楚,比直接丟出方案更有說服力。

4. 用到的積木 / Building Blocks Used

架構即目錄:下表每個決策都對映一個既有 concept note(無 alias 連結);本表即「本案例練到哪些積木」的單一事實來源(single source of truth)。

設計決策 積木 在本題的作用(≤1 行)
容量估算驅動選型 02-Distributed-Systems/05-Numbers-to-Know 算出 >5 天 / 17,520 筆,釘死「不暴力爬 / 不掃 raw」
價格 time-series 高吞吐儲存 06-Database-Tech/03-DynamoDB append-only,product_id 分區 + timestamp 排序
AP / eventual consistency 取向 02-Distributed-Systems/02-CAP-Theorem 可用性優先、放鬆一致性,支撐高寫入
訂閱查詢(門檻 ≥ 新價) 04-Database-Indexing/01-Indexing-Overview secondary index on (product_id, price_threshold)
通知改 event-driven 10-Design-Patterns/05-Real-time-Updates 從 cron pull 改 push,價格變動即時觸發
CDC 串出 DB 變更 09-Messaging-Coord/01-Data-Pipeline tail binlog/WAL 把價格變更轉事件送下游
事件解耦與消費 09-Messaging-Coord/02-Queue 變更事件入 queue,price-change worker 消費
事件 backbone 09-Messaging-Coord/03-Kafka 結構化價格事件的 stream(CDC / dual-write 出口)
通知可靠送達 + 去重 12-Ops-and-Reliability/03-Reliable-Delivery 標記已送出避免重複;至少一次送達
讀路徑預先彙總 10-Design-Patterns/01-Scaling-Reads 每日 batch 算 daily/weekly/monthly,查詢打彙總表
重掃用獨立 read replica 05-Database-Advanced/03-Replication 夜間 heavy full scan 走 replica,不影響線上
analytics 重查詢 06-Database-Tech/04-OLTP-vs-OLAP 跨商品 / heavy join 時導入 column 式 TSDB/OLAP
API 入口路由 11-Infrastructure/01-API-Gateway 把訂閱 / 查詢 request 路由到對應 service
extension / web 取資料 03-API-Design/02-REST GET 價格歷史、POST 訂閱的資源式 API

5. API 設計 (API Design ~3 min)

由 FR 驅動,採 REST 資源風格(理據見 03-API-Design/02-REST)。

Method Path 用途 重點
GET /price/{product_id}?period=30d&granularity=daily 取價格歷史(畫圖表) time-series;打 aggregation 表(非 raw)
POST /subscriptions 建立價格下跌通知訂閱 body: { product_id, price_threshold, notification_type }
細節話術

查詢 API 暴露 granularity(daily / weekly / monthly)與 period——這不是裝飾,而是替後面「按 granularity 命中不同預彙總列」鋪路:30 天圖表打 daily、2 年圖表打 monthly,每張圖只需回數十筆預算好的點。

6. 資料模型與儲存選型 (Data Model & Storage Choice ~4 min)

三張表構成地基:價格時間序列訂閱預彙總。其中價格寫吞吐遠高於訂閱,故刻意拆開。

PriceHistory  (append-only time-series)
--------------------------------------------------
product_id   STRING   PARTITION KEY   ← 同商品聚在一起
timestamp    NUMBER   SORT KEY        ← 時間排序,範圍查詢高效
price        NUMBER
source       STRING   crawler | extension(供驗證 / 信任分級)
--------------------------------------------------
存取模式:依 product_id + 時間範圍掃 → DynamoDB / Cassandra 最適

Subscriptions
--------------------------------------------------
product_id      ─┐ PK = (product_id, user_id)   ← 同商品訂閱聚在一起
user_id         ─┘
price_threshold NUMBER
notification_type STRING
last_notified_at TIMESTAMP            ← 去重 / 避免重複通知
--------------------------------------------------
SQL: secondary index (product_id, price_threshold, user_id)
NoSQL: partition=product_id, sort=(price_threshold, user_id)
查詢:WHERE product_id=:pid AND price_threshold >= :new_price

PriceAggregation  (預先彙總,供 < 500ms 查詢)
--------------------------------------------------
product_id   STRING
granularity  STRING   daily | weekly | monthly
window       DATE/RANGE
avg_price / min_price / max_price  NUMBER
--------------------------------------------------
index on (product_id, granularity, date) → 毫秒回數十筆

儲存選型:

選項 適配本題嗎 理由
DynamoDB / Cassandra ✅ 價格主選 append-only、高寫吞吐、time-series 存取(product_id 分區 + timestamp 排序)
Relational 或 NoSQL(訂閱) ✅ 訂閱用 寫吞吐低;要 (product_id, price_threshold) 範圍查詢 找觸發者,兩者皆可
預彙總表(OLTP 內或獨立) ✅ 讀用 圖表查 彙總列 而非 raw,配合 index 在毫秒內回
TSDB / OLAP(ClickHouse/InfluxDB) △ 進階 跨商品 / heavy join 等 analytics 才需要;column 式對 sum/avg 極快(見第 8.3)

→ 積木:價格存取見 06-Database-Tech/03-DynamoDB;訂閱 index 見 04-Database-Indexing/01-Indexing-Overview;OLTP vs OLAP 取捨見 06-Database-Tech/04-OLTP-vs-OLAP

邊界

「價格與訂閱拆兩個 DB」是因為 寫吞吐量級差很多(價格來自全網爬取 / extension,訂閱只來自使用者操作)。混在一起會讓低頻訂閱被高頻價格寫拖累。

7. 高階架構 (High-Level Design ~5 min)

把積木串成端到端。本題有三條 flow:查歷史建訂閱價格更新 → 通知

(A) 查價格歷史 flow

Client (web / extension)
   │  GET /price/{pid}?period=30d&granularity=daily
   ▼
Price History Service
   │  查 PriceAggregation(非 raw)→ 依 granularity 命中 daily/weekly/monthly
   ▼
回傳 time-series(數十筆預算好的點)→ 前端直接渲染 chart

(B) 建訂閱 flow

Client → API Gateway → Subscription Service
                          │  INSERT (user_id, product_id, price_threshold)
                          ▼
                       Subscriptions table  → 回傳訂閱成功

(C) 價格更新 → 通知 flow(event-driven,deep dive 8.2 展開)

crawler / extension ──寫──► Price DB
                              │  (CDC tail log  或  dual-write 同步發事件)
                              ▼
                         Message Queue / Kafka  (結構化價格變更事件)
                              ▼
                    Price-Change Worker (consumer)
                              │ 1. 查 Subscriptions:price_threshold >= new_price ?
                              │ 2. 命中 → 發 email;標記 last_notified_at(去重)
                              ▼
                         Email Notification

(D) Scaled 架構(端到端)

        ┌──────────── 資料蒐集(hybrid)────────────┐
  使用者裝 extension ──回報 product_id/價格/metadata──┐
                                                      ▼
  Prioritized Crawler ──補爬長尾 / 未被看的商品──► Price Collection Service
       ▲ (用訂閱數/搜尋數/轉換率排優先)                 │ 寫
       │ 可疑回報→最高優先重爬                           ▼
       └─────────── 驗證完整性 ◄──────────────  Price DB (DynamoDB/Cassandra, append-only)
                                                      │ CDC / dual-write
                                                      ▼
                                              Kafka / Message Queue
                                                      ▼
   Client ─► API Gateway ─┬─► Price History Service ─► PriceAggregation 表
                          │        ▲ 每晚 batch 從 read replica 算彙總
                          └─► Subscription Service ─► Subscriptions DB ◄─ Price-Change Worker
                                                                            │
                                                                            ▼  Email

關鍵 hand-off:蒐集(extension + crawler)→ Price DB(append-only)→ CDC/queue → worker join 訂閱 → 通知;查詢路徑與寫入路徑 靠 PriceAggregation 表解耦(讀不碰 raw、不碰寫負載)。

8. 深入探討 (Deep Dives ~9 min)

不重複原則(link-don't-restate)

通用機制(queue、replication、index、OLAP column 儲存)只在 concept note 解釋;這裡只講「本題怎麼用、選哪個、為什麼」。本題無 home note 的新材料(CDC / log-based CDC / dual-write / 優先式爬取 / 群眾外包蒐集)才 inline 教學並登記到 new_concepts_introduced

8.1 如何有效率地發現並追蹤 5 億商品(招牌取捨)

問題:要追蹤 ~5 億商品、每天新增 ~3000 個,且要 優先更新使用者最常看的商品
為何是瓶頸:Amazon 對單 IP rate limit ≈ 1/sec;即使 1000 IP,掃完一輪 > 5 天 → 資料 stale(見第 3 段估算)。

新材料:優先式爬取 + 群眾外包蒐集(本案例首次 introduce)

  • 優先式爬取 (Prioritized Crawling):商品流量高度集中在少數熱門品。用 訂閱數 / 搜尋次數 / 轉換率(從通知點進去) 當 priority score,讓熱門品更頻繁更新。弱點:cold-start——平台上還沒人搜過的新熱門品仍會延遲。
  • 群眾外包蒐集 (Crowdsourced via browser extension):把 extension 當成 分散式資料蒐集框架。使用者逛 Amazon 時,extension 自動擷取 product_id / 即時價格 / metadata 回傳後端。天然優先涵蓋「真的有人在看」的商品,不需龐大 crawler infra,還能順手 發現新商品

方案 做法 優點 缺點
A. 盲目爬全站 BFS 爬 Amazon 所有頁、解析寫價格 直觀、覆蓋完整 一輪 >5 天、資料 stale、撞 rate limit(不可行)
B. 優先式爬取 用 priority score 先爬熱門品 熱門品更新頻繁、省資源 cold-start:新熱門品尚未被搜過會延遲
C. browser extension 群眾外包 使用者瀏覽時 extension 回報價格 天然優先熱門 / 趨勢、發現新品、省 infra 覆蓋不均(沒人看的商品無資料)、惡意回報風險

選擇與理由:採 hybrid = C + B——extension 群眾外包覆蓋「有人在意」的熱門品,crawler 只負責近期未被 extension 看過的長尾。這把「必須監控數百萬商品」這個最大限制 轉成競爭優勢(用 crowdsourced data 對抗 rate limit),這就是本題 core_tradeoff。對 user-generated data 必須 防惡意回報:不直接採用上傳資料,先 驗證完整性;可疑價格變動(如劇烈下跌)以最高優先權重新爬該頁,數分鐘內處理並即時通知。
→ 積木:09-Messaging-Coord/01-Data-Pipeline(蒐集→寫入的資料管線)、12-Ops-and-Reliability/03-Reliable-Delivery(驗證後可靠進系統)

8.2 如何即時處理價格變動並通知訂閱者

問題:把「價格更新」轉成「跌破門檻者收到通知」,且 1 小時內
為何是瓶頸:原始 high-level 用 每 2 小時 cron——(1) 延遲取決於 cron 頻率;(2) 每次都 昂貴的 full table scan 找最近變動。

新材料:CDC 與 dual-write(本案例首次 introduce)

  • Change Data Capture (CDC):把 DB 裡的 新增 / 修改 / 刪除 即時捕捉成事件,送給下游(cache / 搜尋 / 通知)而不需反覆掃表。
  • Log-based CDC:直接讀 DB 的 transaction log(MySQL binlog / Postgres WAL / Oracle redo log),擷取變更轉成結構化訊息送 Kafka。對 source DB 侵入小、近即時。
  • Dual-writes:price collection service 寫 DB 的同時 也把事件發到 Kafka。好處是能在發事件前 加工——過濾掉使用者不在意的小幅波動、把短時間多次變動 合併 後再發。代價:需處理「寫 DB 成功但發事件失敗」的不一致(CDC 沒這問題,因為事件源自 log 本身)。

方案 做什麼 取捨
原始 cron(pull) 每 2h 掃 Price 表找變動 → join 訂閱 延遲受 cron 綁、每次 full scan 昂貴
CDC(push) tail log → 變更事件 → queue → worker 近即時、無侵入;但事件較「原始」,過濾邏輯放下游
Dual-write(push) 寫 DB 同時發事件,可先過濾 / 合併 聰明過濾(去抖、合併);但要處理 dual-write 不一致

選擇與理由:從 pull 改 push 的 event-driven 架構。流程:(1) 一個 process 持續 tail change log 把衍生事件發到 message queue;(2) price-change worker 消費事件 → 查 Subscription DB 找訂閱者(本質是把 DB 與 event stream 做 join);(3) price_threshold >= new_price 則發通知。並 標記已送出last_notified_at)以 去重。dual-write 適合要做「小波動過濾 / 合併」的場景;CDC 適合要無侵入、純就 log 衍生事件。
→ 積木:10-Design-Patterns/05-Real-time-Updates09-Messaging-Coord/02-Queue09-Messaging-Coord/03-Kafka12-Ops-and-Reliability/03-Reliable-Delivery

8.3 如何快速提供價格歷史查詢(< 500ms)

問題:畫圖表的查詢是 GROUP BY day, avg(price) 這類 analytics query;2 年熱門品有 ~17,520 筆 raw(第 3 段),數百萬使用者同時查不同商品,直接掃 raw 撐不住 500ms
為何是瓶頸:raw time-series 是 row 式 OLTP,跑 aggregation 要掃大量列。

方案 做什麼 取捨
A. 每日 pre-aggregation 每晚 batch 算所有商品 daily/weekly/monthly,存 PriceAggregation 表;查詢打彙總表(30 天→daily、2 年→monthly),index(product_id,granularity,date) freshness 落後最多 24h;多花儲存(隨商品×區間線性成長)
B. TSDB / OLAP(ClickHouse/InfluxDB) column 式儲存,對 sum/avg/aggregation 極快;ingestion 走 OLTP→CDC→connector→TSDB 適合 跨商品查詢 / heavy join 等更廣 analytics;多一套系統

選擇與理由:主選 A(pre-aggregation)——價格追蹤使用者關注的是 歷史趨勢而非即時價,所以 24h 的圖表延遲可接受。為了夜間 heavy full scan 不打到線上,額外開一個 read replica 專供夜掃。若日後出現跨商品 / heavy join 等需求,再走 B:OLTP → CDC → stream → TSDB/OLAP,用 column 式儲存維持效能。
→ 積木:10-Design-Patterns/01-Scaling-Reads(讀預彙總)、05-Database-Advanced/03-Replication(夜掃走 replica)、06-Database-Tech/04-OLTP-vs-OLAP(column 式 OLAP)

9. 擴展與瓶頸 (Scaling & Bottlenecks ~3 min)

主動找出先裂之處並補強。

瓶頸 / 風險 補強手段 積木
暴力爬撞 rate limit、資料 stale hybrid:extension 群眾外包熱門 + crawler 補長尾 + 優先式排程 09-Messaging-Coord/01-Data-Pipeline
新熱門品 cold-start 延遲 extension 即時回報優先進系統;可疑回報 最高優先重爬 12-Ops-and-Reliability/03-Reliable-Delivery
cron 拉取延遲高 + full scan 昂貴 event-driven(CDC / dual-write → queue → worker) 10-Design-Patterns/05-Real-time-Updates09-Messaging-Coord/03-Kafka
圖表查詢掃 raw 撐不住 500ms pre-aggregation 表 + (product_id,granularity,date) index 10-Design-Patterns/01-Scaling-Reads
夜間 heavy full scan 打到線上 專用 read replica 跑夜掃 05-Database-Advanced/03-Replication
價格寫吞吐高、訂閱寫被拖累 拆 DB:價格 DynamoDB/Cassandra(高寫)、訂閱獨立 06-Database-Tech/03-DynamoDB
跨商品 analytics / heavy join 需求 導入 column 式 TSDB / OLAP(OLTP→CDC→TSDB) 06-Database-Tech/04-OLTP-vs-OLAP
重複通知 worker 發送後 標記 last_notified_at 去重 12-Ops-and-Reliability/03-Reliable-Delivery
drive 的關鍵句

「系統會 先在『資料蒐集』與『通知延遲』兩處裂開:暴力爬會 stale,所以走 hybrid;cron 拉取太慢且昂貴,所以改 CDC event-driven。讀路徑用 pre-aggregation 卸載,寫路徑靠 append-only NoSQL 撐高吞吐。」

10. 取捨總表與收尾 (Trade-off Recap & Wrap-up ~2 min)

決策點 選擇 放棄了什麼 為什麼值得
資料蒐集 extension 群眾外包 + 優先式 crawler 暴力爬的均勻覆蓋、資料純淨度 熱門品自然優先、新鮮、不被 rate limit 卡(招牌取捨
通知觸發 event-driven(CDC / dual-write) cron 的簡單 近即時 < 1h、免每次 full scan
歷史查詢 每日 pre-aggregation 表 圖表即時新鮮度(落後 ≤24h) < 500ms、趨勢場景可接受延遲
價格儲存 DynamoDB/Cassandra(append-only) 強一致 / 關聯式彈性 高寫吞吐 + time-series 存取(AP 取向)
DB 拆分 價格 / 訂閱分庫 單庫的簡單 寫吞吐量級差太大,避免互相拖累
一致性 eventual(AP) 強一致 價格追蹤可接受短暫不一致,換可用性
收尾話術

「整題單一錨點是 資料蒐集的 hybrid:我們不去『對抗』Amazon 的 rate limit,而是把 使用者瀏覽行為 變成 crowdsourced 資料源,天然優先熱門品,crawler 只補長尾。在這之上,通知改 event-driven 達成 < 1h圖表用 pre-aggregation 達成 < 500ms價格存 append-only NoSQL 撐高寫。代價是覆蓋不均與要防惡意回報——我用『先驗證、可疑高優先重爬』兜底。」引用既有決策用連結,不重新推導。

後續追問演練 (Interviewer Follow-ups — "What if…")

為什麼不暴力爬遍 5 億個 Amazon 頁面?

為什麼通知要從 cron 改成 event-driven?

圖表查詢怎麼做到 < 500ms?

CDC 和 dual-write 差在哪?

一個新商品突然爆紅(trending),但平台上還沒人搜過它,怎麼避免通知延遲?

使用者惡意回報假價格(偽造暴跌)觸發大量誤通知怎麼辦?

流量放大 100x(價格事件、查詢都暴增),先在哪裂開?怎麼補?

為什麼價格存 NoSQL(DynamoDB/Cassandra)而不是關聯式?

pre-aggregation 落後 24h,可不可以接受?什麼情況下不行?

同一商品在很短時間內價格抖動好幾次(如 99→100→99),會不會狂發通知?

主動回想:白板重建 (Whiteboard from Scratch)

不看上文,照下面提示把整個設計重建一次。每格都該能 30 秒內口述。

常見陷阱 (Exam Traps for This Problem)