「設計一個即時聊天系統」(WhatsApp / Messenger / Slack)是大廠系統設計常考題。它的核心挑戰有三個:① 即時雙向通訊、② 海量並發長連線、③ 訊息要可靠且有序地送達(含離線)。新手常一上來就畫資料庫,但這題的靈魂在「連線怎麼維持、訊息怎麼從 A 找到 B 並保證送達」。
這篇用分層拆解 + 面試官追問,把這道題講清楚。
⏱️ 設計主軸:聊天系統不是請求-回應,而是伺服器要主動推訊息給使用者。所以第一個要解的問題是:「怎麼維持雙向連線」與「A 傳的訊息怎麼知道 B 連在哪台伺服器上」。
Step 1 — 釐清需求
- 功能:一對一?群聊?已讀回條、在線狀態、訊息歷史、傳檔、推播通知?
- 規模:多少日活、多少並發連線(百萬級長連線是設計重點)、每秒訊息量。
- 可靠性:訊息不可遺失、要有序、離線也要收得到。
- 延遲:即時(< 數百 ms 送達)。
🎤 面試官追問:「一對一和群聊的設計差在哪?」(群聊要 fanout 給 N 個成員、大群要特別處理。)「訊息可以遺失嗎?」(不行——需要持久化 + 送達確認 + 離線補送。)
Step 2 — 為什麼用 WebSocket(連線方式)⭐
伺服器要主動把訊息推給使用者,傳統 HTTP 請求-回應做不到。選項:
| 方式 | 原理 | 問題 |
|---|---|---|
| 短輪詢 Polling | 客戶端每隔幾秒問一次「有新訊息嗎」 | 延遲高、浪費請求 |
| 長輪詢 Long Polling | 請求 hold 住直到有訊息才回 | 比輪詢好,但每次仍要重連 |
| WebSocket | 一條持久雙向連線,伺服器可隨時推 | 首選;但要管理海量長連線 |
結論:用 WebSocket 建立持久連線;伺服器有訊息就直接從這條連線推給客戶端。
🎤 面試官追問:「為什麼不用 HTTP 輪詢?」(即時性差、請求浪費;WebSocket 一條連線雙向推送最有效率。)「百萬連線怎麼撐?」(用多台**連線伺服器(WebSocket gateway)**水平擴展,每台維持一部分連線。)
Step 3 — 核心難題:A 的訊息怎麼找到 B ⭐
使用者分散連在不同的連線伺服器上。A 在 server-1、B 在 server-3,A 傳訊息給 B 時,系統怎麼知道「B 連在 server-3」並把訊息送過去?
- 連線/Session 註冊表:每個使用者連上時,把「user → 哪台連線伺服器」寫進一個共享的註冊表(如 Redis)。
- 訊息流:A 的訊息 → 連線伺服器 → 聊天服務(Chat Service) → ① 寫進訊息 DB(持久化)② 查註冊表找到 B 在 server-3 → ③ 透過內部訊息匯流排(或直接 RPC)把訊息推到 server-3 → server-3 用 B 的連線推給 B。
- B 離線:註冊表查無 B → 訊息已持久化,透過推播通知(APNs/FCM) 告知,B 上線後從 DB 拉未讀。
🎤 面試官追問:「怎麼知道使用者連在哪台?」(連線時寫入 Redis 的 session 註冊表
user → server。)「連線伺服器之間怎麼傳訊息?」(內部訊息佇列/匯流排,或服務發現 + RPC。)
Step 4 — 訊息儲存與分片
- 資料模型:訊息表(message_id、conversation_id、sender、內容、時間戳、序號)。
- 儲存選型:訊息是寫多、依會話時間排序讀取——適合寬欄式(Cassandra / HBase) 或專用儲存,依
conversation_id分片。 - 分片鍵:用
conversation_id,讓同一個對話的訊息落在同一片,方便依時間拉取歷史。 - 歷史讀取:依會話 + 時間範圍分頁拉取(最近訊息熱、可快取)。
🎤 面試官追問:「為什麼用 Cassandra 而非 MySQL?」(訊息量巨大、寫入密集、依會話時間範圍查詢——寬欄式天生適合。)「分片鍵選什麼?」(conversation_id,讓單一對話落單片。)
Step 5 — 訊息順序、已讀回條、可靠送達 ⭐
- 訊息順序:用每個會話一個遞增序號(sequence ID) 保證有序;客戶端依序號排序、補洞。(全域時間戳因多機時鐘不一致不可靠。)
- 送達狀態:已發送(sent)→ 已送達(delivered)→ 已讀(read) 三態,靠回條(ack)更新。
- 可靠送達(至少一次 + 冪等):訊息至少送一次,可能重複 → 客戶端用 message_id 去重(冪等)。離線訊息持久化在 DB,上線補拉。
🎤 面試官追問:「怎麼保證訊息不亂序?」(每會話遞增 sequence ID,別依賴跨機時間戳。)「重複送達怎麼辦?」(client 用唯一 message_id 去重——這幾乎必問。)「已讀回條怎麼做?」(B 讀取後回 ack,更新狀態並通知 A。)
Step 6 — 群聊 Fanout 與在線狀態
- 群聊 fanout:A 發到群組 → 聊天服務查群成員 → 對每個在線成員推送、離線成員走推播/離線佇列。小群直接 fanout;超大群(如萬人)要特別策略(如不即時 fanout,改成讀取時拉)。
- 在線狀態(Presence):客戶端定期送 heartbeat;presence 服務(Redis)記錄最後心跳,逾時視為離線。狀態變化推送給好友。
🎤 面試官追問:「萬人大群怎麼 fanout?」(避免一次推給所有人——可用 fanout-on-read 或限制即時推送範圍。)「在線狀態怎麼維護?」(heartbeat + Redis TTL,逾時離線。)
架構鳥瞰
使用者A ──WS── 連線伺服器1 ┐ ┌ 連線伺服器3 ──WS── 使用者B
├─► 聊天服務 ──┤
(Session 註冊表: user→server, Redis) │
├─► 訊息 DB(Cassandra, 依 conversation 分片)
├─► 內部訊息匯流排(跨連線伺服器投遞)
└─► 推播服務(APNs/FCM, 給離線用戶)
一張表收束
| 模組 | 解什麼 | 面試送分點 |
|---|---|---|
| WebSocket | 即時雙向推送 | 為何不用輪詢;連線伺服器水平擴展 |
| Session 註冊表 | A 的訊息找到 B | user→server 存 Redis |
| 訊息儲存 | 海量、依會話讀 | Cassandra、conversation 分片 |
| 順序/回條 | 有序、可靠 | 每會話 sequence ID;message_id 去重 |
| 群聊/Presence | fanout、在線 | 大群策略;heartbeat + TTL |
回答框架口訣
被問「設計聊天系統」時照這條線走:
- 釐清:一對一/群聊、規模、可靠性要求
- 連線:為何 WebSocket(伺服器主動推)+ 連線伺服器水平擴展
- 路由:核心難題——Session 註冊表(user→server)讓 A 的訊息找到 B
- 儲存:訊息 DB(Cassandra,依 conversation 分片)
- 可靠:每會話 sequence ID 保序 + message_id 去重 + 已讀回條 + 離線推播
- 進階:群聊 fanout(大群策略)、Presence(heartbeat)
記住一句話:聊天系統的靈魂不是資料庫,而是「如何維持海量雙向連線」與「訊息如何從 A 可靠且有序地送到 B(含離線)」。 把連線路由與送達保證講透,這題就贏了。