Airbnb 訂房平台 (Design Airbnb Booking Platform)
設計 Airbnb 的 訪客訂房流程:依 城市 + 日期區間 搜尋房源、查看房源詳情、在指定日期 預訂。系統有 雙人格——搜尋 / 看房是 read-heavy、availability-first(要快、要撐旺季);訂房是 consistency-first(絕不能 double booking)。
招牌取捨:訂房用 「邏輯可用性」(reserved + expiration_time) 取代實體鎖。不用悲觀鎖長持 transaction(5 分鐘付款窗會吃爆 DB、引發 deadlock),也不依賴 cron 即時翻狀態;而是把「是否可訂」算成 status=available OR (status=reserved 且已過期) 的邏輯判斷,靠 單一 conditional write(atomic transaction) 搶位。cron 只是事後清理實體狀態,它延遲了也不影響正確性——這就是本題的決勝洞察。其餘決勝點:inventory 以 (home_id, date) 為粒度預先產生未來 6–12 個月(把搜尋變成 range scan)、Elasticsearch + CDC 解搜尋延遲、cache + stateless 水平擴展 撐 400K QPS。
配速(35 min 預算):~5 min 釐清需求 → ~4 min 容量估算 → ~3 min API + ~5 min 資料模型(inventory 是地基)→ ~6 min 高階架構(三條 flow)→ ~9 min deep dive(double booking 是重頭戲)→ ~4 min 擴展瓶頸 → ~2 min 收尾。開場先一句話複述、宣告「搜尋走 availability、訂房走 consistency」這條主軸,並把 double booking 標記為「壓軸 deep dive」——宣告配速與主軸本身就是高分動作。
1. 釐清需求與範圍 (Clarify & Scope ~5 min)
開場後先框邊界,別急著畫架構。Airbnb 是巨型系統,主動縮小範圍(聚焦訪客訂房)本身就是成熟度信號。建議問題:
| 釐清問題 | 為何要問 / 影響到哪個決策 | 本題假設(依來源) |
|---|---|---|
| 聚焦誰的視角?host 還是 guest? | 決定範圍大小與核心 flow | 聚焦 guest 訂房流程 |
| 搜尋條件是什麼?地點怎麼表示? | 決定搜尋索引與 query 形狀 | city name + date range(地點先用城市名簡化) |
| 房源詳情要含 images / reviews 嗎? | 決定是否要 blob / CDN / 評論子系統 | 省略 image / review,只留文字資訊 |
| 訂房對一致性的要求?能容忍重複訂嗎? | 直接決定本題核心(鎖 / 一致性設計) | 絕不可 double booking → consistency-first |
| 搜尋的延遲要求? | 決定要不要 Elasticsearch / 預算索引 | search latency < 500ms |
| 規模 / 流量型態?有旺季尖峰嗎? | 驅動估算與擴展設計 | 要撐 peak season,read-heavy |
| 付款要自己做嗎? | 決定是否引入第三方 / 法規範圍 | 委託第三方(Stripe / Adyen),不自建 |
「怎麼避免 double booking 是這題的靈魂,我先標記為壓軸 deep dive——現在先把 booking 當成『檢查 inventory→寫 booking→串付款』的端到端流程畫出來,等高階架構成形後,再回頭把『並發搶位』這塊做深。」把最難的點推遲到 deep dive,先保住敘事連貫。
2. 功能性與非功能性需求 (FR / NFR 與設計北極星)
-
功能性需求 (FR)
- 使用者依 city + date range 搜尋 listings(房源),支援 pagination。
- 使用者查看 home details(文字資訊:city / address / type / amenities)。
- 使用者在 指定日期 book a home(含串接第三方付款)。
-
非功能性需求 (NFR)
- 搜尋 / 看房:availability-first(優先可用性、低延遲)。
- 訂房:consistency-first——絕不 double booking。
- Scalability:撐 peak season 流量尖峰。
- Search latency < 500ms。
-
範圍外 (Out of Scope):host 後台、付款流程內部、推薦系統。
設計北極星(NFR 直接導出後續所有選型的判準):
| NFR | 導出的設計北極星 | 後續對映選型 |
|---|---|---|
| 搜尋 availability + <500ms | 讀路徑避開 full scan,能 range scan / 打索引 / 打 cache | inventory 預算化、Elasticsearch、cache |
| 訂房 consistency / 不重複訂 | 訂房必須是原子的『搶位』,且不長持鎖 | conditional write + reserved/expiration |
| 撐旺季尖峰 | stateless 水平擴展 + cache 擋讀 | stateless service、Redis cache、read replica |
| 雙人格(讀快 / 寫準) | 讀寫路徑解耦、各自最佳化 | Search Service vs Booking Service 拆開 |
「這是一個 讀路徑要快(availability)、寫路徑要準(consistency)的雙人格系統」——搜尋與訂房各走一條優化線,後面每個選型都能從這句推回去。
3. 容量估算 (Back-of-Envelope ~4 min)
估算的目的是 驅動 cache / 索引 / 擴展決策(估算 → 選型的 hand-off)。回連 02-Distributed-Systems/05-Numbers-to-Know。
讀流量(瀏覽 / 搜尋 QPS)
假設 10M DAU,每人平均瀏覽 1 小時 (= 3600 s)
同時在線概念 QPS ≈ 10,000,000 × 3600 ÷ 86,400 ≈ 417,000 QPS
(源頭概算取整為 ~400K;read-heavy,主戰場)
寫流量(訂房 QPS)
訂房是低頻動作(旅客一次旅程訂 1~數次)
write throughput 相對讀 << 1% → 寫不是吞吐瓶頸
但寫的「正確性」(no double booking) 才是難點,不是吞吐
Inventory 儲存(預算化的代價)
inventory 以 (home_id, date) 為一筆
N homes × 未來 ~365 天/home → 列數 = N × ~365
(預先產生未來 6–12 個月的 date rows)
→ 寫入與儲存成本變高,換來搜尋變成簡單 range scan
下表 QPS / 列數皆為 推導值(由 DAU × 瀏覽時長、homes × 預產天數算出,來源僅給出 10M DAU、1 小時瀏覽、6–12 個月預產等輸入)。
| 維度 | 數字 | 對後續決策的意義 |
|---|---|---|
| 瀏覽 / 搜尋 QPS | ≈ 400K QPS(推導值) | 必須靠 cache + stateless 水平擴展 扛,不能每次打 DB |
| 訂房 QPS | 遠低於讀(推導值) | 寫吞吐不是瓶頸;並發正確性 才是難點 |
| Inventory 列數 | N homes × ~365(推導值) | 預算化把搜尋變 range scan,代價是寫入 / 儲存成本上升 |
| 讀寫比 | 極度 read-heavy | 讀路徑是所有快取 / 擴展的重心 |
400K QPS 全是讀 → 訂房的難點 不是吞吐而是並發正確性。這把面試重心精準導向 deep dive:搜尋解「延遲 / 吞吐」,訂房解「double booking」。
4. 用到的積木 / Building Blocks Used
架構即目錄:先給導覽圖,再展開。下表每個決策都對映既有 concept note(無 alias 連結);本表即「本案例練到哪些積木」的單一事實來源(single source of truth)。
| 設計決策 | 積木 | 在本題的作用(≤1 行) |
|---|---|---|
| 容量估算驅動選型 | 02-Distributed-Systems/05-Numbers-to-Know | 算出 ~400K 讀 QPS,決定 cache + stateless 擴展 |
| 讀寫優先級對立(CAP 取捨) | 02-Distributed-Systems/02-CAP-Theorem | 搜尋偏 AP、訂房偏 CP,同系統內按路徑切 |
| 服務入口路由 | 11-Infrastructure/01-API-Gateway | API Gateway 把請求分派到 Home / Search / Booking Service |
| REST endpoint 設計 | 03-API-Design/02-REST | 以資源(home / booking)為中心定義搜尋 / 詳情 / 訂房 |
| 訂房不可 double booking | 12-Ops-and-Reliability/01-Dealing-With-Contention | 並發搶同一 inventory,核心是 contention 處理 |
| 原子搶位(單筆 conditional write) | 05-Database-Advanced/01-Transactions | 用 transaction 把「檢查可用 + 標 reserved」做成原子操作 |
| 悲觀鎖的替代評估 | 09-Messaging-Coord/04-Distributed-Lock | 對照悲觀鎖 / 分散式鎖,本題刻意不長持鎖 |
| 搜尋避免 full scan | 04-Database-Indexing/01-Indexing-Overview | 對 city / date 建索引,減少掃描列數 |
| 全文 / 複雜搜尋 | 08-Search-and-AI/03-Elasticsearch | inverted index + fuzzy match 把搜尋推到 <500ms |
| 搜尋引擎與主庫同步 | 09-Messaging-Coord/01-Data-Pipeline | CDC 把 Postgres 變更串流到 Elasticsearch |
| read-heavy 快取 | 07-Caching-Storage/01-Caching | home data 變動少,home_id→home_object 高 hit rate |
| 快取選型 / 失效 | 07-Caching-Storage/04-Redis | Redis 存熱門 home;DB trigger invalidate 避免 stale |
| 讀擴展三層卸載 | 10-Design-Patterns/01-Scaling-Reads | cache + read replica 擋住 400K 讀 |
| stateless 水平擴展 | 02-Distributed-Systems/03-Scalability | 各 service 無狀態,旺季加機器即擴容 |
| 容錯與讀擴展 | 05-Database-Advanced/03-Replication | read replica 分攤搜尋 / 看房讀流量 |
| 進階地理搜尋 | 04-Database-Indexing/05-Geospatial-Index | landmark 半徑搜尋用 PostGIS / Redis GEO |
| 第三方付款委託 | 03-API-Design/05-API-Security | 委託 Stripe/Adyen,避免自存卡號的合規 / 安全負擔 |
5. API 設計 (API Design ~3 min)
由 FR 驅動,採 REST 資源風格(理據見 03-API-Design/02-REST)。
| Method | Path | 用途 | 重點 |
|---|---|---|---|
GET |
/home/search?city={city}&startDate={s}&endDate={e}&pageSize={n}&page={p} |
搜尋房源 | 走 Search Service,回 Home[],支援 pagination |
GET |
/home/:homeId |
取得房源詳情 | 回 { id, city, address, type, amenities[] },可走 cache |
POST |
/home/book?homeId={id}&startDate={s}&endDate={e} |
預訂房源 | 走 Booking Service → 檢查 inventory → 串第三方付款 |
搜尋 endpoint 內建 pagination——400K QPS 規模下,回傳整批 listings 會撐爆頻寬與延遲;分頁讓每次 query 都是有界的 range scan。
6. 資料模型與儲存選型 (Data Model & Storage Choice ~5 min)
核心是 三張表,其中 Inventory Table 是整題地基(搜尋與訂房都圍著它轉),故獨立成段。
Home (房源詳情,read-heavy / 變動少)
--------------------------------------------------
home_id PK
city VARCHAR (INDEX,搜尋條件)
address VARCHAR
type VARCHAR
amenities JSON/ARRAY
--------------------------------------------------
Inventory (★地基★,粒度 = 每間房每一天)
--------------------------------------------------
(home_id, date) 複合 PK ← 一房一天一列
status ENUM available | reserved | booked
expiration_time TIMESTAMP ← 僅 reserved 時有意義(搶位倒數)
INDEX (city, date) / (home_id, date) ← 把搜尋變 range scan
--------------------------------------------------
列數 = N homes × 未來 6~12 個月天數 (需 cron 定期向前預產)
Booking (訂單)
--------------------------------------------------
booking_id PK
home_id FK
guest_id FK
start_date / end_date
status ENUM created | paid
created_at
--------------------------------------------------
儲存選型:
| 選項 | 適配本題嗎 | 理由 |
|---|---|---|
| 關聯式 DB(PostgreSQL) | ✅ 主選 | 訂房需 ACID transaction + conditional write;inventory 是結構化、要強一致;天生適合 |
| Elasticsearch(搜尋輔助) | ✅ 輔助 | 搜尋走 inverted index + fuzzy,<500ms;經 CDC 與 Postgres 同步 |
| Redis(cache) | ✅ 輔助 | home data 變動少,home_id→home_object 高 hit rate;進階亦可做 geo |
| NoSQL KV(如 DynamoDB) | △ 也行 | 可承載,但訂房的多列原子 check-and-set 在關聯式更直接(見 deep dive) |
把 inventory 攤成 (home_id, date) 每天一列、預先產生未來 6–12 個月,是本題一個刻意取捨:搜尋因此變成簡單的 WHERE city=? AND date IN range AND status=available 的 range scan + filter(換來 <500ms),代價是 寫入與儲存成本上升(要持續 cron 向前補新日期的列)。這是「用空間 / 寫入換讀延遲」的典型。
→ 積木:DB 選型理據見 06-Database-Tech/02-PostgreSQL;搜尋走 08-Search-and-AI/03-Elasticsearch;索引見 04-Database-Indexing/01-Indexing-Overview;快取見 07-Caching-Storage/04-Redis。
7. 高階架構 (High-Level Design ~6 min)
把積木串成端到端。三條 flow(看房 / 搜尋 / 訂房),再看 scaled 架構。
(A) 看房詳情 flow(read-heavy / availability-first)
Client ── GET /home/:homeId ──► API Gateway ──► Home Service
│ 1. 先查 Cache:home_id → home_object ?
│ ├─ HIT → 直接回
│ └─ MISS → 查 Home Table → 回填 cache
▼
回 { id, city, address, type, amenities }
(home data 變動少 → hit rate 高;DB trigger 在更新時 invalidate cache 避免 stale)
(B) 搜尋 flow(availability-first / <500ms)
Client ── GET /home/search?city&dateRange&page ──► API Gateway ──► Search Service
│
▼
查 Inventory:WHERE city=? AND date IN [range] AND status=available
(複合 index → range scan + filter,非 full scan)
│
▼
pagination → 回 Home[]
(規模放大時改打 Elasticsearch,主庫經 CDC 同步)
(C) 訂房 flow(consistency-first / 不可 double booking)
Client ── POST /home/book?homeId&dateRange ──► Booking Service
│
│ 1. 【原子搶位】BEGIN TXN:
│ 檢查 date range 內每列 status=available OR (reserved 且已過期)
│ 若全可用 → UPDATE status=reserved, expiration_time = now + 10min
│ 新增 Booking (status=created)
│ COMMIT TXN ← 任一列搶不到 → rollback,回「已被訂」
│
│ 2. 導向第三方 Payment Service (Stripe / Adyen) 輸入卡號
│
▼ 3. Payment 回成功:
Booking.status = paid
Inventory.status = booked
Payment 失敗 / 逾時:
Inventory 回 available(或等過期 + cron 清理)
(D) Scaled 架構(端到端)
┌──────────────────────────────────────────────┐
Client ───────────► │ API Gateway / LB │
└───────┬───────────────┬───────────────┬──────┘
│ │ │
┌───────▼────┐ ┌───────▼─────┐ ┌──────▼──────┐
│Home Service│ │Search Service│ │Booking Svc │ ← 皆 stateless,旺季加機器
│(availability)│ │(availability)│ │(consistency)│
└──┬─────┬───┘ └──────┬───────┘ └──────┬──────┘
│ │ │ │
┌───────▼─┐ ┌─▼────────┐ ┌──▼───────────┐ │ 原子 conditional write
│ Redis │ │Read │ │Elasticsearch │ │
│ Cache │ │Replicas │ │(inverted idx)│ │
└─────────┘ └────┬──────┘ └──────▲───────┘ │
│ │ CDC ▼
┌────────▼───────────────┴─────────────────────┐
│ PostgreSQL (Home / Inventory / Booking) │
│ Write node ──(replication)──► Read Replicas │
└───────────────────────────────────────────────┘
▲
Cron job:(a) 向前預產未來日期的 inventory 列
(b) 清理過期 reserved → available
Payment:Booking Svc ─► 第三方 (Stripe/Adyen) ─► callback
關鍵 hand-off:Gateway 按 path 分派三個 stateless service → 讀路徑(Home/Search)打 cache / replica / Elasticsearch;寫路徑(Booking)走 write node 做原子 conditional write;搜尋引擎靠 CDC 與主庫解耦同步。
8. 深入探討 (Deep Dives ~9 min)
通用機制(transaction 隔離、cache 策略、CDC、inverted index 原理)只在 concept note 解釋;這裡只講「本題怎麼用、選哪個、為什麼」。任一機制超過 ~3 行就砍掉改連結。本題無 home note 的新材料(邏輯可用性 vs 實體狀態、inventory 預產、付款委託)才在此 inline 教學並登記到 new_concepts_introduced。
8.1 並發訂房:如何在不長持鎖的前提下避免 double booking(壓軸)
問題:多位使用者在 重疊日期 搶同一 home,最終只能一人成功;且付款窗很長(~分鐘級),不能讓使用者「付完款才發現被搶」。
為何是瓶頸:付款是慢的外部步驟。若用悲觀鎖把 inventory 鎖到付款回來,DB 會被長持 transaction 拖垮;若不鎖,又會 double booking。
| 方案 | 做法 | 取捨 |
|---|---|---|
| A.(不佳)Pessimistic Locking | SELECT FOR UPDATE 鎖住 inventory rows,直到付款回應才釋放 |
長持 transaction(~5 min lock)吃爆 DB 資源、lock contention / deadlock(A 鎖 09/01-02、B 鎖 09/04-05 互等);Postgres 無原生 txn 內 lock timeout,要 app 層管理 |
| B.(較好)reserved + expiration + cron | booking 時把列改 reserved、expiration=now+10min;付款成功→booked、失敗→available;cron 掃過期 reserved 翻回 available |
鎖窗短、不長持 txn;但 過期與 cron 執行之間有固有時間落差,且 cron 故障 / 延遲會嚴重影響訂房 |
| C.(最佳)邏輯可用性 > 實體狀態 | 把「可訂」定義成 status=available OR (status=reserved 且 expired);單一原子 txn 內:檢查此邏輯條件 → 改 reserved, expiration=now+10min → commit |
cron 仍存在但 只做事後實體清理;即使 cron 延遲,系統行為也正確(搶位判斷不依賴 cron 翻好狀態) |
選擇與理由:採 C。關鍵洞察是 「一張 ticket 的真實可用性 = 實體狀態 + 是否過期 的組合,是個邏輯判斷,不是某個欄位的當下值」。把它做成 單筆原子 conditional write(在一個 transaction 裡同時驗證邏輯可用 + 標 reserved),就不需要:(1) 長持鎖等付款;(2) 依賴 cron 即時翻狀態。cron 仍要跑(過期 reservation 不會自動翻回 available),但它 延遲了也不影響正確性——這正是 logical availability > physical status 的精髓,也是真實 booking 系統的常見做法。這是本題的 core_tradeoff。
→ 積木:12-Ops-and-Reliability/01-Dealing-With-Contention、05-Database-Advanced/01-Transactions、對照 09-Messaging-Coord/04-Distributed-Lock
一般人直覺把 inventory 的 status 欄位當「唯一真相」,於是必須有人(cron)即時把過期的 reserved 翻回 available,否則房源「卡住沒人能訂」。邏輯可用性 反過來:在 讀取判斷時就 inline 算過期——available := (status=='available') OR (status=='reserved' AND now > expiration_time)。如此 真相在判斷邏輯裡,不在欄位的瞬時值裡,cron 只是把實體欄位「對齊」到邏輯狀態以利清理 / 報表,它的延遲不會讓任何房源被卡住或被重複訂。
悲觀鎖的成本與 持鎖時長 成正比。短交易(毫秒級)持鎖很短,contention 有限。但訂房的鎖要橫跨 使用者輸入卡號的數分鐘付款窗——這是「人類時間」尺度,遠超 DB 樂見的持鎖時長,會放大 contention 與 deadlock,並讓寶貴 DB 連線被閒置佔用。所以這裡要把「鎖」換成「短交易 + 帶過期的 reservation」。對照 09-Messaging-Coord/04-Distributed-Lock 與 05-Database-Advanced/01-Transactions。
8.2 搜尋延遲:從 full scan 到 <500ms
問題:SELECT home_id FROM Inventory WHERE city=? AND start_date=? AND end_date=? 隨 homes 成長會變成 full table scan,越來越慢;未來還要支援多欄位查詢。
為何是瓶頸:400K 讀 QPS 下,每次掃全表既慢又貴,直接撞穿 <500ms 紅線。
| 手段 | 做什麼 | 取捨 |
|---|---|---|
| 對 city / date 建索引 | 把欄位值對映到列,減少掃描列數;配合避免 SELECT *、用 LIMIT 減回傳量 |
幾乎無腦上;但 每個 index 拖慢寫入(寫時要同步更新),需在 index 數與寫效能間平衡 |
| Elasticsearch | inverted index 把每個詞對映到出現的 documents → 快速定位;支援 fuzzy / 部分匹配 | 強大且擅長高流量複雜查詢;但 要與主庫經 CDC 同步(複雜、要保一致)、多一套 cluster 的維運成本 |
選擇與理由:先建 index 保底(任何查詢不全表掃描),當搜尋需求變複雜 / 流量更大時 引入 Elasticsearch。ES 經 CDC(Change Data Capture) 連到 authoritative store(Postgres / DynamoDB),把變更串流同步到搜尋索引——主庫負責真相、ES 負責搜尋,兩者解耦。代價是同步可靠性與 cluster 維運成本,需要可靠的 CDC pipeline。
→ 積木:04-Database-Indexing/01-Indexing-Overview、08-Search-and-AI/03-Elasticsearch、09-Messaging-Coord/01-Data-Pipeline
8.3 撐旺季:400K 讀 QPS 怎麼扛
問題:旺季 ~400K QPS 全是讀,不能每個 request 都打 DB。
為何是瓶頸:read-heavy 的典型死法——讀流量壓垮 DB。
選擇與理由:(1) cache layer——home_id→home_object,因 home data 變動少 → hit rate 高;miss 才打 DB 並回填;用 DB trigger invalidate cache(home 更新時失效,避免 stale)。(2) stateless service——三個 service 都無狀態,旺季 直接水平加機器。(3) read replica 分攤搜尋 / 看房讀流量。寫路徑(訂房)吞吐低,不是瓶頸,但要保 consistency(見 8.1)。
→ 積木:07-Caching-Storage/01-Caching、07-Caching-Storage/04-Redis、10-Design-Patterns/01-Scaling-Reads、02-Distributed-Systems/03-Scalability、05-Database-Advanced/03-Replication
8.4 進階地理搜尋(landmark 半徑)
問題:目前只支援「依城市」搜尋。若要「某地標 X 公里內的 homes」呢?
| 方案 | Pros | Cons |
|---|---|---|
| 1. 第三方地圖 SDK / API | 方便直接,MVP 首選 | 用量大時成本高 |
| 2. Geospatial DB(PostGIS) | 不需新 service / 外部依賴 | 實作較費時;讀流量高時要加 read replica / cache 常見查詢 |
| 3. 獨立 cache + geo index(Redis GEO) | in-memory 回應快 | 額外費用 + 維運;Airbnb 寫吞吐不高,未必值得 |
選擇與理由:實務上 Option 1 → Option 2,只有當 geo query 成為核心瓶頸才上 Redis GEO。因為 Airbnb 的 write throughput 不高,獨立 geo cache 的成本未必划算——能說「先評估便宜方案、別過早上重武器」本身就是成熟度信號。
→ 積木:04-Database-Indexing/05-Geospatial-Index(同源延伸閱讀:13-Case-Studies/02-Earthquake-Notification-System 的 geospatial 設計,一般推論)
8.5 付款為什麼委託第三方
問題:要不要自建付款?
選擇與理由:除非公司有專門支付部門,否則 基於 compliance 委託 Stripe / Adyen——它們有更高等級的安全標準保存卡號,替我們扛下 PCI-DSS 等合規負擔。Booking Service 只負責把使用者導去付款頁、接 callback 更新狀態,自己不碰卡號。
→ 積木:03-API-Design/05-API-Security
9. 擴展與瓶頸 (Scaling & Bottlenecks ~4 min)
主動找出系統會先在哪裂開並補強。
| 瓶頸 / 風險 | 補強手段 | 積木 |
|---|---|---|
| 旺季讀流量壓垮 DB | cache(home_id→object,高 hit rate)+ read replica 分攤讀 | 07-Caching-Storage/01-Caching、05-Database-Advanced/03-Replication |
| 搜尋 full scan 變慢 | index → Elasticsearch(inverted index + CDC 同步) | 08-Search-and-AI/03-Elasticsearch、09-Messaging-Coord/01-Data-Pipeline |
| 並發訂房 double booking | 邏輯可用性 + 單筆原子 conditional write(不長持鎖) | 12-Ops-and-Reliability/01-Dealing-With-Contention、05-Database-Advanced/01-Transactions |
| cron 故障 / 延遲導致房源卡住 | 邏輯可用性把正確性與 cron 解耦——cron 只做事後清理,延遲不影響搶位 | 12-Ops-and-Reliability/01-Dealing-With-Contention |
| inventory 列無限增長 / 未來日期沒列 | cron 定期向前預產 未來 6–12 個月的 (home_id, date) 列 |
10-Design-Patterns/02-Scaling-Writes |
| service 算力不足 | stateless → 水平加機器,前面擺 Gateway / LB | 02-Distributed-Systems/03-Scalability、11-Infrastructure/01-API-Gateway |
| 熱門城市 / 熱門 home(hot partition) | 熱門 home data 進 cache / 多副本;搜尋結果分頁 + 邊快取 | 07-Caching-Storage/04-Redis、10-Design-Patterns/01-Scaling-Reads |
| ES 與主庫不一致 | 可靠 CDC pipeline;主庫為真相,ES 僅供搜尋 | 09-Messaging-Coord/01-Data-Pipeline |
「系統會 在兩處先裂開:讀路徑(400K QPS 全打 DB)→ 用 cache + replica + ES 卸載;訂房並發(double booking)→ 用邏輯可用性 + 原子 conditional write。寫吞吐很低,所以 訂房的難點是正確性不是吞吐。」先指出最可能裂開處再補強,比逐項羅列更有說服力。
10. 取捨總表與收尾 (Trade-off Recap & Wrap-up ~2 min)
| 決策點 | 選擇 | 放棄了什麼 | 為什麼值得 |
|---|---|---|---|
| 訂房並發控制 | 邏輯可用性 + 原子 conditional write | 悲觀鎖的直覺 / 狀態即時翻新的乾淨 | 不長持鎖、付款前就鎖位、cron 延遲不影響正確(招牌取捨) |
| inventory 結構 | (home_id, date) 每天一列、預先產生 |
較低的寫入 / 儲存成本 | 搜尋變 range scan,換到 <500ms |
| 搜尋引擎 | index 保底 → Elasticsearch + CDC | ES cluster 維運 / 同步複雜度 | inverted index + fuzzy,撐高流量複雜查詢 |
| 讀擴展 | cache + read replica + stateless | 額外複雜度 / 成本 | read-heavy 400K QPS 的硬需求 |
| 一致性策略 | 搜尋 availability / 訂房 consistency | 全系統統一一致性模型的簡單 | 雙人格:讀要快、寫要準,各取所需 |
| 付款 | 委託第三方(Stripe/Adyen) | 自建付款的掌控度 | 合規 / 安全交給專業,少扛 PCI 負擔 |
| geo 搜尋 | 第三方 → PostGIS,必要才 Redis GEO | 一步到位的炫技 | 寫吞吐低,避免過早上重武器 |
「整題的單一錨點是 邏輯可用性取代實體鎖:訂房既要 consistency(不 double booking)又不能長持鎖卡死 DB,所以我把『可用』算成 available OR 過期的 reserved,用一筆原子 conditional write 搶位,cron 只負責事後清理、延遲了也不影響正確。其餘是兩條標準線——搜尋走 availability(inventory 預算化 + index/ES + cache)、讀擴展走 stateless + replica 撐旺季。geo / 付款都先用便宜方案,不過早上重武器。」引用既有決策用連結,不重新推導。
後續追問演練 (Interviewer Follow-ups — "What if…")
能——一致性是 按路徑 決定的。搜尋 / 看房就算偶爾看到稍舊的可用性也無妨(大不了訂房時再被擋下),所以優先低延遲 / 高可用。訂房一旦 double booking 就是真實傷害(兩人同房),必須強一致。把兩條路徑拆成不同 service、各自選模型即可。見 02-Distributed-Systems/02-CAP-Theorem。
不把 inventory 的 status 欄位當唯一真相,而在判斷時 inline 算過期:available := status=='available' OR (status=='reserved' AND now > expiration_time)。真相在判斷邏輯裡。cron 只把實體欄位對齊邏輯狀態(清理),它延遲了也不會卡住房源或造成重複訂。見 12-Ops-and-Reliability/01-Dealing-With-Contention。
10M DAU × 3600 s(每人瀏覽 1 小時)÷ 86,400 s ≈ 400K QPS(推導值)。幾乎全是讀,所以重心在 cache + read replica + stateless 擴展。回連 02-Distributed-Systems/05-Numbers-to-Know。
讀路徑先裂。依序:(1) 提高 cache 命中(Redis 擴容 / 預熱熱門城市與 home);(2) 增加 read replica;(3) 搜尋全面走 Elasticsearch 並擴 cluster;(4) service stateless → 直接加機器,前面 Gateway/LB 分流。訂房寫吞吐低,主要確保 write node 不被熱門 home 的並發搶位拖慢(見下一題)。見 10-Design-Patterns/01-Scaling-Reads、02-Distributed-Systems/03-Scalability。
這是寫端 hot row contention。手段:(1) 搶位是 短交易(毫秒級),天然限制單列被佔時長,遠優於悲觀鎖長持;(2) 失敗者立即收到「已被訂」可重試別的日期 / 房源;(3) 若單列仍成熱點,可在應用層做 排隊 / 限流,或把該 home 的 inventory 列做更細的分區。核心仍是 12-Ops-and-Reliability/01-Dealing-With-Contention,並對照 12-Ops-and-Reliability/02-Overload-Protection。
訂房仍正確——因為搶位用「邏輯可用性」inline 算過期,不依賴 cron 把過期 reserved 翻回 available。影響只是:(1) 過期列的實體 status 暫時還是 reserved(報表 / 觀感層面),(2) 若同時負責「向前預產未來日期 inventory 列」的 cron 也掛了,超出已預產範圍的日期會搜不到房源。前者無害,後者需監控 cron 健康(12-Ops-and-Reliability/04-Observability)。這正是把正確性與 cron 解耦的價值。
付款失敗 → Booking Service 主動把列改回 available。使用者直接關頁(沒有 callback)→ 不主動回收也沒關係:expiration_time 一到,該列在 下一個搶位判斷 就被視為「過期的 reserved = 可用」,被新使用者搶走;cron 事後再把實體 status 對齊成 available。兩條回收路徑(主動 + 過期)都不依賴長持鎖。見 05-Database-Advanced/01-Transactions。
inventory 的真相本就在 DB(Postgres),用 DB 自身的原子 transaction + conditional write 搶位,可一步到位地「驗證 + 標記」且天然持久化,無需引入第二個一致性來源。分散式鎖會多一個外部依賴、又要處理鎖過期 / 持有者崩潰 / 鎖與 DB 狀態不同步的問題。除非搶位邏輯橫跨多個資料源,否則 DB 內原子操作更簡單可靠。對照 09-Messaging-Coord/04-Distributed-Lock。
預產的代價是寫入 / 儲存上升、且要 cron 持續向前補列;回報是 搜尋變成有索引的 range scan(<500ms)與訂房的 逐日原子搶位 變直接。替代做法是用「booking 區間表 + 查詢時做區間重疊判斷」,省儲存但讓搜尋 / 搶位的查詢變複雜(要做 range overlap)。在 read-heavy + 嚴格延遲需求下,用空間 / 寫入換讀延遲與邏輯簡單性 通常划算。見 10-Design-Patterns/02-Scaling-Writes。
使用者可能搜到「ES 顯示可用、實際剛被訂走」的房源。這在搜尋路徑可接受(搜尋本就 availability-first,允許稍舊資料),因為 訂房時的原子 conditional write 是最後的守門員——真正搶位以 Postgres 為準,過期 / 已訂的會在訂房當下被擋下。所以 ES 的最終一致性不會造成 double booking,只會偶爾讓使用者點進去發現已訂。見 09-Messaging-Coord/01-Data-Pipeline。
主動回想:白板重建 (Whiteboard from Scratch)
不看上文,照下面提示把整個設計重建一次。每格都該能 30 秒內口述。
常見陷阱 (Exam Traps for This Problem)
What:BEGIN TXN → SELECT FOR UPDATE 鎖 inventory → 等使用者付款 → 才 commit/release。
Why:付款是 人類時間(分鐘級) 的慢步驟,長持 transaction 吃爆 DB 連線 / 資源,並引發 lock contention 與 deadlock(A 鎖 09/01-02、B 鎖 09/04-05 互等);Postgres 還沒原生 txn 內 lock timeout。
正確做法:短交易 + 帶 expiration 的 reservation,用 邏輯可用性 + 原子 conditional write 搶位。見 12-Ops-and-Reliability/01-Dealing-With-Contention、05-Database-Advanced/01-Transactions。
What:booking 標 reserved,完全靠 cron 把過期的翻回 available 才能再被訂。
Why:cron 與過期之間有 固有時間落差;cron 故障 / 延遲就會卡住房源,沒人能訂——正確性被 cron 綁架。
正確做法:把可用性算成 available OR 過期的 reserved(邏輯判斷),cron 僅事後清理;cron 延遲不影響搶位正確性。見 12-Ops-and-Reliability/01-Dealing-With-Contention。
SELECT * ... WHERE city AND date(full table scan)
What:不建索引、不分頁,直接掃 Inventory 全表過濾。
Why:homes 一多就是 full scan,400K QPS 下又慢又貴,撞穿 <500ms。
正確做法:把 inventory 預產成 (home_id, date) 列 + 複合 index 變 range scan;複雜 / 大流量改 Elasticsearch + CDC;用 LIMIT + 分頁限回傳量。見 04-Database-Indexing/01-Indexing-Overview、08-Search-and-AI/03-Elasticsearch。
What:流程是「拿訂單→去付款→回來才檢查 inventory 是否還可用」。
Why:使用者 付款成功卻訂不到(已被搶),體驗極差,還要額外處理 void payment(退款)。
正確做法:付款前 就用原子 conditional write 把位子標 reserved(帶 10 min 過期),再導去付款;付款只負責把 reserved→booked。見 05-Database-Advanced/01-Transactions。
What:在自己 DB 存卡號、自建付款流程。
Why:PCI-DSS 等 合規 / 安全負擔極重,沒有專門支付部門不值得自扛。
正確做法:委託 Stripe / Adyen,自己不碰卡號,只接 callback 更新 booking 狀態。見 03-API-Design/05-API-Security。
What:一開口就「地理搜尋上 Redis GEO 獨立 cache」。
Why:Airbnb write throughput 不高,獨立 geo cache 的費用 / 維運未必划算,屬過早優化。
正確做法:第三方地圖 API → PostGIS,只有當 geo query 成核心瓶頸才導入 Redis GEO。見 04-Database-Indexing/05-Geospatial-Index。
Related Notes
- 02-Distributed-Systems/05-Numbers-to-Know
- 02-Distributed-Systems/02-CAP-Theorem
- 02-Distributed-Systems/03-Scalability
- 03-API-Design/02-REST
- 03-API-Design/05-API-Security
- 04-Database-Indexing/01-Indexing-Overview
- 04-Database-Indexing/05-Geospatial-Index
- 05-Database-Advanced/01-Transactions
- 05-Database-Advanced/03-Replication
- 06-Database-Tech/02-PostgreSQL
- 07-Caching-Storage/01-Caching
- 07-Caching-Storage/04-Redis
- 08-Search-and-AI/03-Elasticsearch
- 09-Messaging-Coord/01-Data-Pipeline
- 09-Messaging-Coord/04-Distributed-Lock
- 10-Design-Patterns/01-Scaling-Reads
- 10-Design-Patterns/02-Scaling-Writes
- 11-Infrastructure/01-API-Gateway
- 12-Ops-and-Reliability/01-Dealing-With-Contention
- 12-Ops-and-Reliability/02-Overload-Protection
- 12-Ops-and-Reliability/04-Observability
- 13-Case-Studies/02-Earthquake-Notification-System