設計模式練習題 (Practice - Design Patterns)
Related Concepts
- 10-Design-Patterns/01-Scaling-Reads
- 10-Design-Patterns/02-Scaling-Writes
- 10-Design-Patterns/03-Long-Running-Tasks
- 10-Design-Patterns/04-Handling-Large-Blobs
- 10-Design-Patterns/05-Real-time-Updates
| 模式 | 預設做法 | 關鍵取捨 / 觸發條件 |
|---|---|---|
| 讀取擴展 | index → denormalize → replica → cache | 讀寫比高、內容型應用;> 50~100k 讀取/秒 |
| 寫入擴展 | 垂直 → sharding → queue → batching | 爆發流量、寫入密集;選對 partition key |
| 長任務 | queue + worker pool(立刻回 job ID) | 操作 > 幾秒;最終一致性 |
| 大型檔案 | Presigned URL 直傳 + CDN download | > 10MB;事件通知 + 對帳同步狀態 |
| 即時更新 | SSE(單向)/ WebSocket(雙向) | 先問「輪詢夠嗎」;兩個 hop |
Part 1:Scaling Reads(讀取擴展)
Q1(Recall):讀取擴展的三步演進順序是什麼?
- Database 內部優化:index、硬體升級、denormalization、materialized view
- 水平擴展:read replica、sharding
- 外部 caching layer:application cache(Redis)、CDN edge cache
重點:很多人直接跳到分散式 caching,卻沒先把更簡單的 index/replica 用盡。
Q2(Recall):Denormalization 的本質取捨是什麼?什麼時候不該用?
用空間換速度:允許資料重複儲存,避免大量 join → 讀取更快,但寫入更複雜(改名要更新多處)。
寫入也頻繁時不該用 —— 這個複雜度可能不划算。決定前先確認讀寫比。
Q3(Recall):Cache invalidation 的五種策略分別是什麼?
- TTL:固定存活時間到期失效(更新可預期的資料)
- Write-through:寫 DB 時同步更新/刪 cache(需一致性,增寫入延遲)
- Write-behind:失效事件排 queue 非同步處理(降寫入延遲)
- Tagged:給 entry 加標籤,相關資料變動整批失效(複雜依賴)
- Versioned keys:key 內含版本號,更新即遞增(簡單可靠)
Q4(Application):設計短網址服務(QR Code Generator),讀寫比極度不平衡(縮短一次、點擊幾百萬次)。你的讀取擴展策略?
這是最適合 caching 的場景:
- Redis 積極快取「短網址 → 長網址」對應,不需設 TTL(短網址不會改變)
- 搭配 CDN 處理全球流量
- 實際 database 只在 cache miss 時才被查到
不需要複雜的 replica/sharding —— cache 命中率極高就解決了。
Q5(Application):「幾百萬請求同時讀同一個 hot key」(名人發文),cache server 開始 timeout。兩個解法?
- Request Coalescing(請求合併):對同一 key 的多個請求合併成一個 → backend 請求量從「無限」降到 N(app server 數量)
- Cache Key Fanout(key 扇出):把 hot key 複製成 10 份(
feed:taylor:1~:10),client 隨機挑 → 500k req/s 分散成每 key 50k
Fanout 代價:記憶體增加 + 失效時要清所有副本(TTL 要錯開)。
Q6(Analysis):解釋 Cache Stampede 的成因,並比較三種解法的優劣。
成因:TTL 到期是二元的 —— 這秒資料還在、下秒全沒。熱門 entry 過期瞬間,所有請求同時 miss 打 DB(自己對自己 DDoS)。
| 解法 | 機制 | 優劣 |
|---|---|---|
| Distributed Lock | 第一個 miss 拿鎖重建,其他等待 | 保護 backend,但重建失敗/太久會讓幾千請求 timeout,高負載脆弱 |
| Probabilistic Early Refresh | entry 越舊,觸發背景刷新機率越高 | 較佳,把刷新分散開,多數用戶仍拿到快取 |
| 背景主動刷新 | 持續在過期前更新最熱資料 | 保證永不觸發重建,但基礎設施複雜、可能刷新沒人要的資料 |
Q7(Analysis):面試官問「你會用 cache 提升讀取效能」,但補充「我們的 DB 負載沒問題,只是延遲偏高」。你該怎麼回應?
要區分兩個不同問題:
- 讀取擴展(scaling reads)是關於減少 DB 負載 —— 適用 DB 撐不住的情況
- 「DB 負載沒問題、只要更低延遲」是不同問題,需不同解法:edge computing、service mesh 優化、把資料移到離用戶更近的地方
盲目加 cache 不一定對症。先確認瓶頸是吞吐量還是延遲。
Part 2:Scaling Writes(寫入擴展)
Q8(Recall):寫入擴展的四個策略是什麼?
- 垂直擴展 + Database 選型
- Sharding 與 Partitioning
- Queue + Load Shedding(處理爆發)
- Batching + 階層式聚合(改變寫入結構)
Q9(Recall):為什麼 Cassandra 的寫入吞吐量遠高於傳統 relational database?代價是什麼?
Cassandra 用 append-only commit log,把資料順序寫入磁碟(而非 in-place update 那種昂貴的磁碟尋址)→ 普通硬體就能 10k+ 寫入/秒(傳統 RDB 約 1k)。
代價:讀取效能差 —— 讀取常需檢查多個檔案並合併結果,可能比索引良好的 RDB 還慢。典型的「讀寫張力」取捨。
Q10(Recall):Horizontal Sharding 和 Vertical Partitioning 的差異?
- Horizontal Sharding:分 row —— 把資料列分散到多個 shard(如
hash(userID)) - Vertical Partitioning:分 column —— 把不同存取模式的欄位拆成專門的表(貼文內容 vs 互動數字 vs 分析資料),再移到不同 DB 各自優化
Q11(Application):你要對社群媒體貼文做 sharding。面試官問「partition key 怎麼選」。請完整回答,並說明選錯的後果。
選 hash(userID) 或 hash(postID) —— 高基數、均勻分佈。
原則:選讓各 shard 寫入量差異最小的 key。
選錯的後果:若用「用戶國家」當 key,人口多的中國 shard 過載、紐西蘭 shard 閒置 → 失去 10× server = 10× 效能的目標。
同時要考慮讀取:若每個讀取都要打所有 shard 收集資料,讀取 overhead 很大。問「這請求要打幾個 shard?多常發生?」
Q12(Application):Robotaxi 系統,幾萬輛車每幾秒回報位置,寫入暴增。為什麼 Load Shedding 比加 Queue 更合適?
加 queue 只會讓 queue 爆掉(持續以比 DB 快的速度寫入)。
Load Shedding 更合適,因為位置資料的特性:用戶每隔幾秒就再打一次 API。丟掉一筆,幾秒後就有更新鮮的資料!
系統過載時直接丟棄「距上次更新只有幾秒」的位置 → 保住系統運作,丟掉的也不重要。
Q13(Application):爆紅推文每秒 10 萬讚,即使均勻 sharding 仍壓垮所在 shard。兩種拆 hot key 的方法?
- 固定拆分所有 key(split all keys):每 key 拆成 k 份(
post1Likes-0~-k-1),單 shard 寫入降 k 倍。缺點:資料/讀取量都 ×k - 動態拆分 hot key:偵測到熱才拆成 sub-key(如 100 個),讀取時聚合
只適用可聚合 metrics(讚/觀看/計數),不適用須原子的資料(profile)。讀寫者都要知道哪些 key 熱 → 大多數生產系統用「讀者永遠檢查所有 sub-key」(簡單)。
Q14(Analysis):解釋 Hierarchical Aggregation 如何解決「直播留言幾百萬人 all-to-all」問題。
問題:幾百萬人寫留言/按讚,每筆都要傳給其他所有人 → all-to-all 無法解決。
關鍵洞察:所有觀看者想要的是同一個最終一致的視圖 → 聚合視圖可遞增建立。
- Write processor:依留言 ID 路由,在時間窗口內聚合按讚數 → 一批轉發給 root processor(只做合併)
- Broadcast node:用 consistent hashing 把用戶分配到 broadcast node,原本寫 N 個觀看者 → 寫 M 個 node,由 node 轉發
大幅降低任一系統的寫入量,代價是增加延遲。
Q15(Analysis):Resharding(8 → 16 shard)如何在不停機下完成?
天真做法(下線、重 hash、搬移)對大資料集意味數小時停機。
生產系統用漸進式遷移 + 雙寫(dual-write):
- 同時寫入舊 shard 和新 shard
- 讀取優先讀新 shard
- 在保持可用性的同時漸進遷移資料
雙寫階段確保遷移過程中沒有資料遺失。
Part 3:Manage Long-Running Tasks(長時間任務)
Q16(Recall):這個模式的核心概念是什麼?Web server 和 Worker 各做什麼?
核心:把「接受請求」和「處理請求」解耦。
- Web server:輕量請求路由器 —— 驗證 → 推進 queue → 立刻回傳 job ID(毫秒)
- Worker pool:從共用 queue 拉 job、以自己速度處理、完成後更新狀態
queue 訊息只放 job ID,不放完整資料。
Q17(Recall):這個模式得到什麼、失去什麼(各列兩項)?
得到:快速回應(ms 回 job ID 不 timeout)、獨立擴展(web 用便宜機/worker 用 GPU 機)、故障隔離、更好資源利用。
失去:系統複雜度(queue/worker/狀態追蹤)、最終一致性(回傳時工作沒完成)、job 狀態追蹤基礎設施、監控 overhead。
Q18(Application):面試官說「我們每天處理 100 萬張圖片,每張要 10 秒」。你該怎麼當場反應?
大聲算出來:100 萬 / 86400 秒 ≈ 12 張/秒,每張 10 秒 → 每秒累積 120 秒工作量 → 至少需要 120 個 worker 才跟得上。
「我們不能在 web server 上跑這個 —— 它們會被圖片處理塞滿、無法服務用戶請求。我會 offload 到專用 async worker pool,讓 web 層保持快速,worker 在計算優化硬體上獨立擴展。」
Q19(Application):用戶不耐煩點了三次「產生報告」,queue 裡有三個一樣的 job。怎麼防重複?
用 Idempotency Key(冪等鍵):
- 接受 job 時要求唯一識別碼(user ID + action + timestamp 取整)
- 開工前先查這個 key 的 job 是否已存在 → 有就回現有 job ID,不建新的
工作本身也要冪等(重跑安全)—— 繼續前先確認信件是否已發、檔案是否已處理。
Q20(Application):黑五 job 暴增 10×,worker 跟不上,queue 長到幾百萬。怎麼辦?
Backpressure(背壓):
- 設 queue 深度上限,太深就拒新 job,立刻回「系統忙碌中」(而非接受無法處理的工作)
- 根據 queue 深度自動擴展 worker(不是 CPU!等 CPU 飆高 queue 早就積了)
關鍵 metric 是 queue 深度,CloudWatch alarm + Auto Scaling group。
Q21(Analysis):「有的報告 5 秒、有的 5 小時,全在同一 queue」會造成什麼問題?怎麼解?
問題:長 job 卡住短 job(head-of-line blocking)—— 簡單報告要在某人 5 小時匯出後面等。worker 利用率不均,autoscaling 變噩夢。
解法:按 job 類型/預期時間分 queue:
fastqueue:很多 worker、小機(t3.medium)slowqueue:少但強的 worker、大機(c5.xlarge)
無法預測時間就先進 fast,超時移 slow。或把大 job 拆成小 chunk。
Q22(Analysis):Dead Letter Queue(DLQ)解決什麼問題?為什麼要監控它?
解決 poison message:若不處理,失敗 job 無限重試浪費資源,且讓 worker 崩潰的訊息可能讓整個 worker 群掛掉(每台試處理都死)。
機制:失敗達一定次數(通常 3~5)後移到 DLQ 隔離,健康工作繼續。DLQ 變成需人工調查的 batch。
為何監控:DLQ 持續增加通常代表系統有需立刻處理的 bug。
Part 4:Handling Large Blobs(大型檔案)
Q23(Recall):什麼大小的檔案應該用這個模式?核心做法是什麼?
> 10MB 且不需 SQL 查詢 → Blob Storage(S3)。
核心做法:不讓資料流過你的伺服器,而是給 client **臨時、有範圍限制的憑證(Presigned URL)**直接和儲存服務互動。Server 角色從「資料傳輸者」變「存取控制者」。
Q24(Recall):Presigned URL 產生時可以烘焙進哪些限制條件?為什麼需要?
因為任何人有了 URL 都能用,所以產生時就要把限制編碼進簽名:
- content-length-range:最小/最大檔案大小(防 10MB 端點被傳 10GB)
- content-type:頭像端點只接受圖片、不接受影片
永遠要加檔案大小限制,否則有人可用小圖片 URL 上傳幾 TB 讓儲存費用爆炸。
Q25(Application):上傳 5GB 影片到 99% 時連線斷了。系統應如何避免從頭重來?
用分塊上傳(S3 Multipart,每塊 5MB+):
- 儲存服務追蹤哪些分塊已成功
- 斷線時 client 查已上傳的分塊(
ListParts),從失敗處(第 61 塊)續傳 - client 把 session ID(S3 upload ID)存 localStorage,App 重啟也能續
- 全部傳完後呼叫完成端點組裝;設生命週期政策清理未完成上傳(會收費)
Q26(Analysis):「信任 client 通知上傳完成」會有哪些問題?正確的狀態同步機制是什麼?
信任 client 的問題:
- Race condition:DB 顯示 completed 但檔案還沒到 Storage
- 孤兒檔案:client 上傳後、通知前崩潰
- 惡意 client:沒上傳就標記完成
- 網路失敗:通知沒抵達
正解:事件通知 + 對帳
- 主機制:Storage 事件(S3 → SNS/SQS,帶 storage_key)—— 由儲存服務本身確認,client 移出信任鏈
- 安全網:對帳(reconciliation) —— 定期任務檢查卡在 pending 的檔案,和 Storage 比對
Q27(Analysis):防止上傳濫用最有效的手段是什麼?為什麼比即時偵測更穩固?
最有效手段:不要讓用戶立刻存取上傳的東西。
建處理流水線:上傳先進隔離區(quarantine bucket)→ 病毒掃描/內容驗證/大小檢查 → 通過才移到正式 bucket、DB 標記 available。
為何更穩固:即使有人繞過 rate limiting 上傳惡意內容,在系統批准前他也用不了。處理延遲本身抑制濫用(攻擊者無法立刻知道是否成功,自動化攻擊更難)。
Part 5:Real-time Updates(即時更新)
Q28(Recall):即時系統要解決的「兩個 hop」分別是什麼?
- 第一個 Hop:如何把更新從 server 送達 client?(通訊協定:polling / SSE / WebSocket / WebRTC)
- 第二個 Hop:如何把更新從 事件來源傳播到 server?(觸發機制:pull / consistent hashing / pub/sub)
Q29(Recall):SSE 和 WebSocket 的核心差異?各自適合什麼?
- SSE:單向(server → client),建在 HTTP(chunked)上,瀏覽器原生 EventSource + 自動重連。適合單向推送(AI token 串流、即時儀表板)
- WebSocket:全雙工,TCP(HTTP upgrade),持久有狀態連線。適合高頻雙向(聊天、遊戲)
常見模式:SSE 訂閱更新 + 一般 HTTP POST 寫入,不需急著上 WebSocket。
Q30(Application):為什麼 WebSocket 必須用 L4 負載平衡器,而不是 L7?
L7 LB 終止傳入連線、再對後端建立新連線 → 破壞 WebSocket 的持久連線。
L4 LB 在傳輸層運作,維護 client 和 server 之間同一條 TCP 連線 → 天然支援 WebSocket。
(Long polling 等 HTTP 方案則 L7 更靈活。)
Q31(Application):設計聊天系統,要把訊息送達數百萬連線的用戶。第二個 hop 選 Consistent Hashing 還是 Pub/Sub?
聊天訊息是小訊息、無太多關聯狀態 → 選 Pub/Sub:
- 每用戶一個 topic,client 連到任意端點伺服器並訂閱對應 topic
- 發訊息 → 發布到目標用戶 topic → 廣播給訂閱的端點伺服器 → 轉發給 client
- 端點伺服器負載平衡容易(least connections)
若是 Google Docs 那種需維護大量關聯狀態的協作編輯,才選 Consistent Hashing(連線和特定文件關聯)。
Q32(Analysis):面試官要設計「線上拍賣即時出價」。一個候選人立刻說「我用 WebSocket + WebRTC」。你的評估?
過度設計。應先問「輪詢夠嗎」:
- 出價更新若每 1~2 秒推一次可接受 → Simple Polling / SSE 就夠,複雜度最低
- WebRTC 完全不適合(那是音視訊 P2P,拍賣不需要)
- 真要雙向高頻(搶標最後幾秒)才考慮 WebSocket
Senior+ 面試非常欣賞「能用簡單方案就不過度設計」。用輪詢還能同時迴避兩個 hop。
Q33(Analysis):即時系統中「連線失敗和重連」要處理哪兩件事?
- 偵測斷線:WebSocket 不一定發斷線訊號(殭屍連線:client 以為還連著,server 已清掉)→ 用**心跳(heartbeat)**偵測
- 恢復不遺失資料:追蹤 client 已收到哪些訊息 → 維護按用戶的訊息 queue 或序列號,重連時帶上最後收到的 ID 補發遺漏(Redis Stream 是熱門方案)
| 問題 | 解法 |
|---|---|
| 讀取擴展順序 | index → denormalize → replica → cache |
| hot key 幾百萬讀 | Request Coalescing + Cache Key Fanout(TTL 錯開) |
| cache 過期瞬間打爆 DB | Cache Stampede → Probabilistic Early Refresh / 主動刷新 |
| 寫入擴展四策略 | 垂直選型 / sharding / queue+shedding / batching+聚合 |
| partition key | 高基數均勻分佈(hash(userID)),避免「國家」「時間」 |
| 位置回報過載 | Load Shedding(丟舊的,幾秒後有新的) |
| hot key 寫入壓垮 shard | 拆 sub-key(只適用可聚合 metrics) |
| resharding 不停機 | dual-write 雙寫 + 讀新 shard |
| 操作 > 幾秒 | async worker pool + queue,立刻回 job ID |
| 防重複 job | Idempotency Key |
| queue 暴增 | Backpressure(依 queue 深度擴展,不是 CPU) |
| poison message | DLQ(3~5 次失敗後隔離 + 監控) |
| 檔案 > 10MB | Presigned URL 直傳 + CDN download |
| 上傳 99% 斷線 | Multipart Upload + ListParts 續傳 |
| 上傳狀態同步 | Storage 事件(主)+ 對帳(安全網) |
| 防上傳濫用 | quarantine bucket,掃描通過才可存取 |
| 即時更新兩個 hop | 協定(SSE/WS)+ 傳播(pub/sub / consistent hashing) |
| WebSocket LB | 必須 L4(L7 破壞持久連線) |
| 廣播大量 client | Pub/Sub;需大量狀態 → Consistent Hashing |
| 即時方案優先 | 先問「輪詢夠嗎」,別過度設計 |