近年來,通知系統已成為許多應用程式非常受歡迎的功能。通知會用重要資訊提醒使用者,例如即時新聞、產品更新、活動、優惠等。它已成為我們日常生活中不可或缺的一部分。在本章中,你會被要求設計一個通知系統。
通知不只是行動推播通知。三種通知格式為:
- 行動推播通知(mobile push notification)
- 簡訊(SMS message)
- 電子郵件(Email)
圖 1 顯示這三種通知各自的範例。
Step 1 - 理解問題並確立設計範圍#
建立一個每天可送出數百萬則通知的可擴展系統並不容易。它需要對通知生態系統有深入的理解。這個面試題目刻意設計得開放且模糊,因此你有責任提出問題以釐清需求。
應徵者:系統支援哪些類型的通知?
面試官:推播通知、簡訊與電子郵件。
應徵者:它是即時系統嗎?
面試官:我們把它當作一個軟即時(soft real-time)系統。我們希望使用者能盡快收到通知。然而,若系統處於高負載狀態,輕微的延遲是可以接受的。
應徵者:支援哪些裝置?
面試官:iOS 裝置、Android 裝置以及筆記型/桌上型電腦。
應徵者:什麼會觸發通知?
面試官:通知可由客戶端應用程式觸發,也可在伺服器端排程。
應徵者:使用者能選擇退訂嗎?
面試官:可以,選擇退訂的使用者將不再收到通知。
應徵者:每天會送出多少則通知?
面試官:1000 萬則行動推播通知、100 萬則簡訊以及 500 萬則電子郵件。
Step 2 - 提出高階設計並取得認可#
本節展示支援多種通知類型的高階設計:iOS 推播通知、Android 推播通知、簡訊以及電子郵件。結構如下:
- 不同類型的通知
- 聯絡資訊收集流程
- 通知傳送/接收流程
不同類型的通知#
我們先從高層次了解每種通知類型如何運作。
iOS 推播通知#

我們主要需要三個元件來傳送 iOS 推播通知:
Provider:provider 建立並送出通知請求到 Apple Push Notification Service(APNS)。要建立推播通知,provider 提供以下資料:
Device token:這是用於傳送推播通知的唯一識別碼。
Payload:這是包含通知 payload 的 JSON 字典。以下是範例:
{ "aps":{ "alert":{ "title":"Game Request", "body":"Bob wants to play chess", "action-loc-key":"PLAY" }, "badge":5 } }
APNS:這是 Apple 提供的遠端服務,用以將推播通知傳送到 iOS 裝置。
iOS Device:是最終客戶端,接收推播通知。
Android 推播通知#
Android 採用類似的通知流程。Android 通常使用 **Firebase Cloud Messaging(FCM)**而非 APNs 來傳送推播通知到 Android 裝置。

簡訊#
對於簡訊,常用第三方簡訊服務,例如 Twilio [1]、Nexmo [2] 等。其中大多數是商業服務。

電子郵件#
雖然公司可以自行架設郵件伺服器,但許多公司選擇使用商業電子郵件服務。Sendgrid [3] 與 Mailchimp [4] 是最受歡迎的電子郵件服務之一,它們提供更佳的送達率與資料分析。

圖 6 顯示加入所有第三方服務後的設計。

聯絡資訊收集流程#
要傳送通知,我們需要收集行動裝置的 device token、電話號碼或電子郵件地址。如圖 7 所示,當使用者安裝我們的 App 或首次註冊時,API 伺服器會收集使用者聯絡資訊並儲存到資料庫。

圖 8 顯示用於儲存聯絡資訊的簡化資料表。電子郵件地址與電話號碼儲存在 user 資料表中,而 device token 則儲存在 device 資料表中。
一個使用者可以擁有多個裝置,這表示一則推播通知可送到該使用者所有裝置。
通知傳送/接收流程#
我們會先呈現初始設計,然後再提出一些優化。
高階設計#
圖 9 顯示設計,每個系統元件說明如下。

- Service 1 to N:服務可以是微服務、cron job 或會觸發通知傳送事件的分散式系統。例如,計費服務寄送電子郵件提醒客戶繳費,或購物網站透過簡訊告知客戶包裹明天送達。
- Notification system:通知系統是傳送/接收通知的核心。從簡單做法開始,只使用一台通知伺服器。它為 service 1 到 N 提供 API,並為第三方服務建立通知 payload。
- Third-party services:第三方服務負責將通知送到使用者。在與第三方服務整合時,我們需要特別注意可擴充性。良好的可擴充性意味著系統具備彈性,能輕易接上或移除某個第三方服務。另一個重要的考量是某第三方服務可能在新市場或未來無法使用。例如,FCM 在中國無法使用,因此當地會使用 Jpush、PushY 等替代第三方服務。
- iOS、Android、SMS、Email:使用者在他們的裝置上接收通知。
此設計中發現三個問題:
- 單一故障點(SPOF):單一通知伺服器意味著 SPOF。
- 不易擴展:通知系統把所有與推播通知相關的事都在一台伺服器中處理。難以獨立擴展資料庫、快取以及不同的通知處理元件。
- 效能瓶頸:處理與傳送通知可能耗用大量資源。例如,建構 HTML 頁面與等待第三方服務回應都可能花費時間。把所有事情都在一個系統中處理可能導致系統過載,特別是在尖峰時段。
將資料庫、快取與通知處理邏輯全都綁在單一伺服器,不僅無法獨立擴展,也容易因第三方服務回應緩慢而造成整體瓶頸。
高階設計(改善版)#
列出初始設計中的挑戰後,我們改善設計如下:
- 將資料庫與快取移出通知伺服器。
- 增加更多通知伺服器並設置自動水平擴展。
- 引入訊息佇列以解耦系統元件。
圖 10 顯示改善後的高階設計。

理解上圖最好的方式是由左至右閱讀:
Service 1 to N:代表透過通知伺服器提供的 API 來傳送通知的不同服務。
Notification servers:提供以下功能:
- 提供 API 給服務以傳送通知。這些 API 只能由內部或經驗證的客戶端存取,以防止垃圾訊息。
- 進行基本驗證,例如驗證電子郵件、電話號碼等。
- 查詢資料庫或快取以取得渲染通知所需的資料。
- 將通知資料放入訊息佇列以進行平行處理。
以下是傳送電子郵件的 API 範例:
POST https://api.example.com/v/sms/sendRequest body:
{ "to":[ { "user_id":123456 } ], "from":{ "email":"from_address@example.com" }, "subject":"Hello World!", "content":[ { "type":"text/plain", "value":"Hello, World!" } ] }Cache:使用者資訊、裝置資訊與通知範本被快取。
DB:儲存關於使用者、通知、設定等資料。
Message queues:消除元件間的依賴。當大量通知要送出時,訊息佇列充當緩衝區。每種通知類型分配到不同的訊息佇列,這樣某個第三方服務的中斷不會影響其他類型的通知。
Workers:workers 是一群伺服器,從訊息佇列拉取通知事件並傳送到對應的第三方服務。
Third-party services:已在初始設計中說明。
iOS、Android、SMS、Email:已在初始設計中說明。
接下來,讓我們檢視每個元件如何協同運作以送出一則通知:
- 服務呼叫通知伺服器提供的 API 以送出通知。
- 通知伺服器從快取或資料庫取得 metadata,例如使用者資訊、device token 與通知設定。
- 通知事件被送到對應的佇列進行處理。例如,一個 iOS 推播通知事件會送到 iOS PN 佇列。
- Workers 從訊息佇列拉取通知事件。
- Workers 將通知傳送到第三方服務。
- 第三方服務將通知送達使用者裝置。
Step 3 - 設計深入探討#
在高階設計中,我們討論了不同類型的通知、聯絡資訊收集流程以及通知傳送/接收流程。我們將在深入探討中討論以下內容:
- 可靠性
- 額外元件與考量:通知範本、通知設定、速率限制、重試機制、推播通知中的安全性、監控佇列中的通知以及事件追蹤
- 更新後的設計
可靠性#
在分散式環境中設計通知系統時,我們必須回答幾個重要的可靠性問題。
如何防止資料遺失?#
通知系統最重要的需求之一是不能遺失資料。通知通常可以延遲或重新排序,但絕不能遺失。為了滿足此需求,通知系統會將通知資料持久化在資料庫中並實作重試機制。為了資料持久化,我們加入通知日誌資料庫,如圖 11 所示。
收件者會剛好收到一次通知嗎?#
簡短的答案是否定的。雖然大多數情況下通知會剛好送達一次,但分散式特性可能導致重複的通知。
為了減少重複的發生,我們引入去重(dedupe)機制並謹慎處理每種失敗情況。以下是簡單的去重邏輯:
當一個通知事件首次到達時,我們透過檢查 event ID 來判斷是否已看過。若已看過則丟棄;否則就送出通知。
對於想了解為何無法做到剛好一次傳送的讀者,請參考參考資料 [5]。
額外元件與考量#
我們已經討論了如何收集使用者聯絡資訊、傳送與接收通知。通知系統不只是這些。在這裡我們討論額外的元件,包括範本重用、通知設定、事件追蹤、系統監控、速率限制等。
通知範本#
大型通知系統每天送出數百萬則通知,許多通知遵循相似的格式。我們引入通知範本以避免從頭建立每則通知。通知範本是一個預先格式化的通知,可以透過自訂參數、樣式、追蹤連結等來建立你獨特的通知。
以下是推播通知的範本範例。
BODY:
You dreamed of it. We dared it. [ITEM NAME] is back — only until [DATE].
CTA:
Order Now. Or, Save My [ITEM NAME]使用通知範本的好處包括:
- 維持一致的格式
- 減少錯誤邊際
- 節省時間
通知設定#
使用者每天通常收到太多通知,很容易感到不堪負荷。因此,許多網站與 App 提供使用者對通知設定的細緻控制。這些資訊儲存在通知設定資料表中,包含以下欄位:
user_id bigIntchannel varchar# push notification、email 或 SMSopt_in boolean# 是否選擇接收通知
在傳送任何通知給使用者之前,我們會先檢查該使用者是否選擇接收這類通知。
速率限制#
為避免讓使用者收到太多通知而感到不堪負荷,我們可以限制使用者能接收的通知數量。
這很重要,因為如果送得太頻繁,收件者可能會完全關閉通知。
重試機制#
當第三方服務傳送通知失敗時,該通知會被加回訊息佇列以便重試。如果問題持續存在,會發送警報給開發者。
推播通知中的安全性#
對於 iOS 或 Android App,使用 appKey 與 appSecret 來保護推播通知 API [6]。只有經過驗證或核可的客戶端才能透過我們的 API 發送推播通知。有興趣的讀者請參考參考資料 [6]。
監控佇列中的通知#
一個關鍵指標是佇列中的通知總數。如果該數字很大,表示 workers 處理通知事件的速度不夠快。為避免通知傳送的延遲,需要更多 workers。圖 12(來源 [7])顯示一個待處理佇列訊息的範例。
事件追蹤#
通知指標,例如開啟率、點擊率與互動率,對於了解客戶行為很重要。分析服務實作事件追蹤。通常需要將通知系統與分析服務整合。圖 13 顯示可能為了分析目的而被追蹤的事件範例。
更新後的設計#
把所有元件放在一起,圖 14 顯示更新後的通知系統設計。

在此設計中,相較於先前的設計加入了許多新元件:
- 通知伺服器配備了兩個更關鍵的功能:身份驗證與速率限制。
- 我們也加入重試機制以處理通知失敗。如果系統傳送通知失敗,它們會被放回訊息佇列,workers 將重試預定次數。
- 此外,通知範本提供一致且高效的通知建立流程。
- 最後,加入監控與追蹤系統以進行系統健康檢查與未來改進。
Step 4 - 總結#
通知不可或缺,因為它讓我們持續獲得重要資訊。它可能是 Netflix 上你最愛電影的推播通知、新產品折扣的電子郵件,或關於線上購物付款確認的訊息。
在本章中,我們描述了一個可擴展通知系統的設計,支援多種通知格式:推播通知、簡訊與電子郵件。我們採用訊息佇列來解耦系統元件。
除了高階設計之外,我們深入探討了更多元件與優化。
- 可靠性:我們提出穩健的重試機制以將失敗率降到最低。
- 安全性:使用 AppKey/appSecret 對來確保只有經驗證的客戶端能傳送通知。
- 追蹤與監控:在通知流程的任一階段都實作這些,以擷取重要的統計數據。
- 尊重使用者設定:使用者可選擇退訂通知。我們的系統在傳送通知前會先檢查使用者設定。
- 速率限制:使用者會感謝對通知數量的頻率上限。
恭喜你看到這裡!現在給自己一個鼓勵。做得好!