讀取擴展 (Scaling Reads)
核心問題
讀取流量成長遠快於寫入。 一個 Instagram feed 載入可能觸發 100+ 次讀取,但用戶一天只發一篇(一次寫入)。一般讀寫比至少 10:1,內容型應用可達 100:1 以上。
CPU 指令數、RAM 容量、磁碟 I/O 都有硬上限。碰到瓶頸時,加再多程式碼都沒用 → 必須從架構下手。
解法的演進順序(必背)
1. 先把 Database 自己調好 ── index / 硬體 / denormalization / materialized view
↓ 還不夠
2. 水平擴展 Database ── read replica / sharding
↓ 還不夠
3. 加外部 Caching Layer ── application cache (Redis) / CDN edge cache
很多人直接跳到「分散式 caching」,卻沒先把更簡單的 index / replica 用盡。先從你擁有的開始 —— 現代 DB 在正確 index 下能承受遠超預期的負載。
粗略觸發點:讀取 > 50,000~100,000 次/秒(已有適當 index)才需要考慮水平擴展或加 cache。
第一步:Database 內部優化
| 技巧 | 做法 | 代價 / 注意 |
|---|---|---|
| Index | 在常 query/join/sort 的欄位加索引,O(n)→O(log n) | 「index 太多拖慢寫入」被過度放大;index 太少死掉的應用遠多於太多 |
| 硬體升級(垂直擴展) | SSD(隨機 I/O 快 10~100×)、加 RAM、更快 CPU | 最快買到喘息空間;但面試官通常不想聽這個(迴避核心) |
| Denormalization | 用空間換速度,允許資料重複,避免大量 join | 寫入變複雜(改名要更新多處);先確認讀寫比 |
| Materialized View | 預先算好昂貴的聚合(如平均評分),背景排程更新 | 結果有延遲 |
重複儲存讓讀取更快,但讓寫入更複雜。寫入也頻繁時,這個複雜度可能不划算。
第二步:水平擴展
Read Replica(首選)
- Leader-Follower:所有寫入走 primary,讀取打任一 replica → 分散讀取負載
- 副作用好處:冗余 —— primary 掛了可提升 replica 為新 primary
- 同步 vs 非同步:同步慢但一致,非同步快但短暫不一致
寫入 primary 後資料需時間傳播到 replica。用戶寫完馬上讀,可能讀到還沒同步的 replica → 看不到自己的改動。詳見 05-Database-Advanced/03-Replication。
Sharding
對讀取擴展有兩個好處:資料集變小、負載分散。但 Sharding 主要是寫入擴展技術,運維複雜度高 —— 大多數讀取問題加 cache 更有效。詳見 05-Database-Advanced/02-Sharding、10-Design-Patterns/02-Scaling-Writes。
第三步:外部 Caching Layer
關鍵觀察:存取模式高度偏斜。幾百萬人讀同一則爆紅推文,DB 一直在回答完全一樣、幾乎不變的問題。
| 層次 | 延遲 | 說明 |
|---|---|---|
| Application Cache(Redis/Memcached) | < 1ms | 坐在 app 和 DB 中間,hit 直接回,miss 才查 DB 並回填 |
| CDN / Edge Cache | 個位數 ms | 延伸到全球 edge;可降低 origin 負載 90%+ |
公開貼文、商品目錄、搜尋結果 ✅;個人偏好、私人訊息、帳戶資訊 ❌(hit rate = 0)。詳見 07-Caching-Storage/01-Caching。
Cache Invalidation 五策略
| 策略 | 機制 | 適用 |
|---|---|---|
| TTL | 固定存活時間到期失效 | 更新模式可預期的資料(安全網,5~15 分) |
| Write-through | 寫 DB 時同步更新/刪 cache | 需一致性,但增加寫入延遲 |
| Write-behind | 失效事件排 queue 非同步處理 | 降低寫入延遲,短暫過時 |
| Tagged | 給 entry 加標籤,相關資料變動時整批失效 | 複雜依賴關係 |
| Versioned keys | key 內含版本號,更新即遞增 | 簡單可靠,需追蹤版本 |
需求說「搜尋結果最多 30 秒過時」→ TTL = 30 秒。讓 cache 策略有依據,而不是靠猜。
常見 Deep Dive
1000 萬筆無 index → full table scan(掃 2GB 才找一筆 email)。CREATE INDEX 後 1000 萬次掃描變幾次 index 查找。複合查詢注意欄位順序:(status, created_at) 幫得到「只用 status」和「兩者並用」,但幫不到「只用 created_at」。
傳統 cache 假設負載分散在很多 key,所有人讀同一 key 時假設破裂。
- Request Coalescing:對同一 key 的多個請求合併成一個 → backend 請求量從「無限」降到 N(app server 數量)
- Cache Key Fanout:把一個 hot key 複製成 10 份(
feed:taylor:1~:10),client 隨機挑 → 500k req/s 分散成每 key 50k。代價是記憶體增加 + 失效要清所有副本
TTL 到期是二元的:這秒還在,下秒全沒 → 100k 請求同時 miss 打 DB(自己對自己 DDoS)。
- Distributed Lock:第一個 miss 拿鎖重建,其他等待 → 但重建失敗/太久會讓幾千請求 timeout,高負載下脆弱
- Probabilistic Early Refresh(較佳):entry 越舊,請求觸發背景刷新的機率越高(50 分鐘 1%、55 分鐘 5%、59 分鐘 20%)→ 把刷新分散開
- 背景主動刷新:最熱資料持續在過期前更新,保證用戶永不觸發重建
寫後刪 cache 有 race condition(刪舊值後、寫新值前的請求會把舊資料重算回 cache)。
Cache Versioning:每筆記錄有版本號,更新時在同一 transaction 遞增;cache key 帶版本(event:123:v42 → v43)。舊 entry 不刪、自然失效。無 race(遲到寫入無法覆蓋)、CDN/browser 因版本在 URL 自然過期。代價:每次請求兩次查找(版本 + 資料)。
計算型資料(搜尋/feed)用 Deleted Items Cache:維護小型「最近刪除項目」快取,提供 feed 時過濾掉。
何時用 / 何時不用
| ✅ 適合 | ❌ 不適合 |
|---|---|
| 內容型應用(社群、電商、影音) | 寫入密集(Robotaxi 位置追蹤,讀寫比 2:1)→ 先做寫入擴展 |
| QR/短網址(讀寫極度不平衡) | 小規模(1000 用戶)→ 單一良好 DB 即可,別過度設計 |
| Ticketmaster 活動頁、News Feed、YouTube metadata | 強一致(金融、庫存)→ 需積極失效 + 更短 TTL |
| 即時協作(Google Docs)→ 需即時更新,caching 反而有害 |
讀取擴展是關於減少 DB 負載。如果 DB 負載沒問題、只是要更低延遲,那是不同問題(edge computing / service mesh 優化)。
Related Notes
- 10-Design-Patterns/02-Scaling-Writes — 對偶問題:寫入擴展
- 07-Caching-Storage/01-Caching — Cache-Aside、淘汰策略、Stampede、Hot Key 細節
- 05-Database-Advanced/03-Replication — Read Replica、Replication Lag、Read-After-Write
- 05-Database-Advanced/02-Sharding — Functional / Geographic sharding
- 04-Database-Indexing/01-Indexing-Overview — Index trade-offs
- 02-Distributed-Systems/05-Numbers-to-Know — 50k TPS 讀取上限、量級直覺
- 01-Networking/06-Load-Balancing — CDN、區域分片
- 10-Design-Patterns/Practice-Design-Patterns