Amazon 價格追蹤服務 (Design Amazon Price Tracking Service)
設計一個 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)滿足 < 500ms、append-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 約束) |
「怎麼有效率地爬 5 億個商品 是這題真正的硬核,我先標記成 deep dive;現在先把 crawler 當成一個 會定期把價格寫進 DB 的 black box,這樣我們能先把『查歷史』與『訂閱通知』兩條主 flow 串起來。」把最難的推遲,保住敘事連貫。
2. 功能性與非功能性需求 (FR / NFR 與設計北極星)
-
功能性需求 (FR)
- 使用者能 查看某商品的價格歷史(網站 / Chrome extension),用於 畫圖表。
- 使用者能 訂閱價格下跌通知 並 設定門檻(
price_threshold)。
-
範圍外 (Out of scope):平台內搜尋 / 探索商品、跨零售商比價、評論評分整合。
-
非功能性需求 (NFR)
- 可用性 > 一致性:AP 取向,可接受 eventual consistency。
- 規模:處理 5 億商品。
- 價格歷史查詢 < 500ms。
- 價格變動後 1 小時內 送通知。
設計北極星(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)
通用機制(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 段估算)。
- 優先式爬取 (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(驗證後可靠進系統)
因為 惡意回報風險:使用者可偽造價格觸發錯誤通知。做法是把回報視為「待驗證訊號」——先驗證 integrity,對可疑值(如異常暴跌)送進 crawling system 以最高優先重爬該頁 來確認,確認後才採用並通知。即「使用者回報加速發現,crawler 做權威驗證」。
8.2 如何即時處理價格變動並通知訂閱者
問題:把「價格更新」轉成「跌破門檻者收到通知」,且 1 小時內。
為何是瓶頸:原始 high-level 用 每 2 小時 cron——(1) 延遲取決於 cron 頻率;(2) 每次都 昂貴的 full table scan 找最近變動。
- 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-Updates、09-Messaging-Coord/02-Queue、09-Messaging-Coord/03-Kafka、12-Ops-and-Reliability/03-Reliable-Delivery
事件帶 product_id 與 new_price;worker 拿它去 Subscriptions 表查 WHERE product_id=:pid AND price_threshold >= :new_price。為了這個查詢高效,Subscriptions 才需要 (product_id, price_threshold) 的 secondary index / sort key(見第 6 段)。命中的 user_id 即收件者。
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)
會,但這是 刻意的關注點分離:通知走即時 event-driven(8.2,秒~分鐘級),圖表走預彙總(趨勢,24h 可接受)。使用者在意「現在跌了」靠通知、「過去走勢」靠圖表,兩條路徑不同新鮮度需求、用不同手段,正是本題成熟處。
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-Updates、09-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 |
「系統會 先在『資料蒐集』與『通知延遲』兩處裂開:暴力爬會 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…")
Amazon 對單 IP rate limit ≈ 1/sec;即使 1000 IP,5e8 ÷ 1000 ÷ 86,400 ≈ 5.78 天(推導值)才一輪 → 資料嚴重 stale。所以改 hybrid:extension 群眾外包熱門 + crawler 補長尾。見 09-Messaging-Coord/01-Data-Pipeline。
cron(每 2h)有兩個問題:(1) 延遲綁在 cron 頻率 上,難保 < 1h;(2) 每次都 full table scan 找變動,昂貴。改 CDC / dual-write → queue → worker,價格變更即時推送、免掃表。見 10-Design-Patterns/05-Real-time-Updates、09-Messaging-Coord/03-Kafka。
不掃 raw(2 年熱門品 ~17,520 筆);每晚 batch 預算 daily/weekly/monthly 彙總 存 PriceAggregation 表,查詢依 granularity 命中(30 天→daily、2 年→monthly),靠 (product_id,granularity,date) index 毫秒回數十筆。見 10-Design-Patterns/01-Scaling-Reads。
CDC 讀 DB transaction log(binlog/WAL)衍生事件,無侵入、事件源自 log 故不會「寫了 DB 卻沒發事件」。dual-write 是 service 寫 DB 同時主動發事件,好處是能 先過濾小波動 / 合併多次變動 再發,但要處理寫 DB 成功、發事件失敗的不一致。見 09-Messaging-Coord/01-Data-Pipeline。
這是 cold-start。靠 extension 群眾外包 兜底:只要有使用者逛到它,extension 就回報 product_id / 即時價,系統立即發現並開始追蹤——天然優先「真的有人在看」的趨勢品,不必等搜尋訊號累積。可疑暴跌再 最高優先重爬 驗證。見 12-Ops-and-Reliability/03-Reliable-Delivery。
不直接信 user-generated data:先 驗證完整性;偵測到可疑變動(如劇烈下跌)就把該頁 以最高優先送進 crawler 重爬 取得權威值,確認後才通知。即「extension 加速發現、crawler 做權威驗證」。見 09-Messaging-Coord/01-Data-Pipeline。
(1) 寫路徑:價格事件暴增 → append-only NoSQL(DynamoDB/Cassandra)按 product_id 分區水平擴展;(2) 通知:queue / Kafka 分區擴 consumer(price-change worker),靠 09-Messaging-Coord/02-Queue 削峰;(3) 讀:pre-aggregation 表 + 多 read replica,熱門商品圖表可再加 cache;(4) 跨商品 analytics 變多 → 導入 column 式 OLAP(見 06-Database-Tech/04-OLTP-vs-OLAP)。
存取模式是 append-only 高寫吞吐的 time-series:依 product_id + 時間範圍查。DynamoDB/Cassandra 以 product_id 分區 + timestamp 排序,天生適合這種寫密集 + 範圍掃描,且 AP 取向符合「可用性優先、eventual consistency」。關聯式在這種寫吞吐與分區擴展上較吃力。見 06-Database-Tech/03-DynamoDB、02-Distributed-Systems/02-CAP-Theorem。
對 價格追蹤的圖表(看趨勢)可接受——使用者要的是歷史走勢不是即時價,而即時需求由 通知路徑 滿足。若產品定位變成「即時比價 / 即時看盤」,圖表也要近即時,就得改走 TSDB / OLAP 直接對近 raw 做快速 aggregation(OLTP→CDC→TSDB),用 column 式儲存補延遲。見 06-Database-Tech/04-OLTP-vs-OLAP。
會,若每個事件都直發。這正是 dual-write 的價值:在發事件前 過濾小幅波動 / 合併短時間多次變動,只對「真正跌破門檻且穩定」的變化發事件。再加 worker 端 last_notified_at 去重 / 冷卻。見 12-Ops-and-Reliability/03-Reliable-Delivery。
主動回想:白板重建 (Whiteboard from Scratch)
不看上文,照下面提示把整個設計重建一次。每格都該能 30 秒內口述。
常見陷阱 (Exam Traps for This Problem)
What:BFS 把 Amazon 所有頁爬一遍,定期更新。
Why:rate limit 1/sec,1000 IP 也要 >5 天一輪,資料 stale;且純對抗 rate limit 是死路。
正確做法:hybrid——extension 群眾外包熱門 + 優先式 crawler 補長尾,把限制轉成優勢。見 09-Messaging-Coord/01-Data-Pipeline。
What:每 N 小時掃 Price 表找變動,再 join 訂閱發通知。
Why:延遲綁 cron 頻率(難保 < 1h),每次 full scan 昂貴。
正確做法:event-driven(CDC / dual-write → queue → worker),價格變更即時推送。見 10-Design-Patterns/05-Real-time-Updates。
What:每次畫圖都 GROUP BY day, avg(price) 掃原始資料。
Why:2 年熱門品 ~17,520 筆 × 數百萬使用者 → DB 上的 analytics query 撐不住 500ms。
正確做法:每日 pre-aggregation 表 + (product_id,granularity,date) index;進階用 column 式 TSDB/OLAP。見 10-Design-Patterns/01-Scaling-Reads、06-Database-Tech/04-OLTP-vs-OLAP。
What:把 user-generated 價格資料直接寫 Price DB 並觸發通知。
Why:可被 惡意偽造(假暴跌)觸發誤通知。
正確做法:先 驗證完整性;可疑變動 以最高優先重爬 取權威值再採用。見 12-Ops-and-Reliability/03-Reliable-Delivery。
What:價格時間序列和訂閱塞同一張表 / 同一庫。
Why:價格寫吞吐遠高於訂閱(全網爬取 vs 使用者操作),會把低頻訂閱拖垮。
正確做法:拆庫——價格走 append-only NoSQL(DynamoDB/Cassandra),訂閱獨立。見 06-Database-Tech/03-DynamoDB。
Related Notes
- 02-Distributed-Systems/05-Numbers-to-Know
- 02-Distributed-Systems/02-CAP-Theorem
- 03-API-Design/02-REST
- 04-Database-Indexing/01-Indexing-Overview
- 05-Database-Advanced/03-Replication
- 06-Database-Tech/03-DynamoDB
- 06-Database-Tech/04-OLTP-vs-OLAP
- 09-Messaging-Coord/01-Data-Pipeline
- 09-Messaging-Coord/02-Queue
- 09-Messaging-Coord/03-Kafka
- 10-Design-Patterns/01-Scaling-Reads
- 10-Design-Patterns/05-Real-time-Updates
- 11-Infrastructure/01-API-Gateway
- 12-Ops-and-Reliability/03-Reliable-Delivery