招商銀行2面:如何實現(xiàn)一個通知系統(tǒng)?
在實際工作中,我們經(jīng)常會用到通知系統(tǒng),比如,用戶完成在線購買后,需要發(fā)送訂單確認(rèn)郵件、支付處理成功的短信以及包裹發(fā)貨的推送通知。那么,什么是通知系統(tǒng)?如何設(shè)計一個通知系統(tǒng)?這篇文章,我們來聊一聊!

需求收集
在設(shè)計之前,我們先來詳細(xì)了解下通知系統(tǒng)的需求,本文從功能需求和非功能需求兩個方面來介紹。
1.功能需求
- 通知類型:例如消息通知、警告通知、活動通知等。
 - 用戶群體:需要通知的用戶群體是誰,是否有分組。
 - 通知渠道:例如郵件、短信、推送通知、應(yīng)用內(nèi)通知等。
 - 通知頻率:通知的發(fā)送頻率和限流策略。
 - 優(yōu)先級:不同通知的優(yōu)先級管理。
 - 用戶偏好:用戶是否可以自定義接收通知的偏好。
 - 重試機制:處理通知發(fā)送失敗的情況,必要時重試(如短信或電子郵件發(fā)送失?。?。
 
2.非功能需求
- 可擴展性:系統(tǒng)應(yīng)能夠每分鐘處理數(shù)百萬條通知,支持?jǐn)?shù)百萬并發(fā)用戶。
 - 高可用性:確保最小的停機時間,即使在故障情況下也能發(fā)送通知。
 - 可靠性:保證至少一次的通知傳遞,對于某些使用場景可能需要保證只有一次傳遞。
 - 低延遲:通知應(yīng)盡快發(fā)送,以確保及時交付。
 
容量預(yù)估
在深入設(shè)計之前,讓我們先估算下系統(tǒng)規(guī)模以更好地做出設(shè)計決策。假設(shè)系統(tǒng)服務(wù)于 1000萬日活用戶,每個用戶平均每天接收 5條通知。
1.峰值負(fù)載
假設(shè)在峰值時間內(nèi)(如秒殺期間)1分鐘內(nèi)發(fā)送 100萬條通知,這意味著系統(tǒng)應(yīng)能夠處理:
- 每天的通知數(shù)量:10,000,000 x 5 = 50,000,000條通知
 - 峰值每秒通知數(shù)量:1,000,000 / 60 = ~17,000條通知/秒
 
2.存儲需求
假設(shè)每條通知的數(shù)據(jù)量大小是 1KB,則存儲容量評估為:
- 用戶數(shù)據(jù)存儲需求:10,000,000 * 1 KB = 10GB
 - 每日通知存儲需求:10,000,000 * 5 * 1 KB = 50GB
 
High-level 設(shè)計
從 High-level 層面來看,通知系統(tǒng)將包括以下組件:

(1) 通知服務(wù)(Notification Service)
通知服務(wù)是所有通知請求的入口,無論是來自外部應(yīng)用程序還是內(nèi)部系統(tǒng)。它暴露的API可以供各種客戶端調(diào)用以觸發(fā)通知。
這些請求可以是發(fā)送事務(wù)性通知(如密碼重置郵件)、促銷通知(如折扣優(yōu)惠)或系統(tǒng)警報(如停機警告)。
每個請求都被驗證以確保其包含所有必要的信息,如接收者ID、通知類型、消息內(nèi)容以及應(yīng)通過哪些渠道發(fā)送通知(電子郵件、短信等)。
對于需要在未來日期或時間發(fā)送的通知,通知服務(wù)與調(diào)度服務(wù)(Scheduler Service)集成。
處理請求后,通知服務(wù)將通知推送到通知隊列(如 Kafka或 RabbitMQ)。
(2) 用戶偏好服務(wù)
用戶偏好服務(wù)允許用戶控制如何接收通知。
它存儲和檢索用戶接收不同渠道通知的個人偏好。
服務(wù)跟蹤用戶明確選擇加入或退出的通知類型。
例如:用戶可以選擇退出營銷或促銷內(nèi)容。
為防止用戶被通知淹沒,用戶偏好服務(wù)對某些類型的通知(尤其是促銷消息)實施頻率限制。
例如:用戶每天只能接收2條促銷通知。
(3) 調(diào)度服務(wù)
調(diào)度服務(wù)負(fù)責(zé)存儲和跟蹤定時通知——那些需要在特定未來時間發(fā)送的通知。
這些可以包括提醒、促銷活動或其他不立即發(fā)送但必須基于預(yù)定時間觸發(fā)的時間敏感通知。
例如:促銷消息可能計劃在下周發(fā)送。
一旦到達預(yù)定時間,調(diào)度服務(wù)將從其存儲中提取通知并將其發(fā)送到通知隊列。
(4) 通知隊列
通知隊列在通知服務(wù)和渠道處理器之間充當(dāng)緩沖區(qū)。
通過將通知請求提交與通知發(fā)送解耦,隊列使系統(tǒng)能夠更有效地擴展,尤其是在高流量期間。
隊列系統(tǒng)提供消息傳遞的保證。
根據(jù)使用場景,可以配置為:
- 至少一次傳遞:確保每條通知至少發(fā)送一次,即使這在罕見情況下會導(dǎo)致重復(fù)消息。
 - 只有一次傳遞:確保每條通知只發(fā)送一次,防止重復(fù),同時保持可靠性。
 
(5) 渠道處理器
渠道處理器負(fù)責(zé)從通知隊列中提取通知并通過特定渠道(如電子郵件、短信、推送通知和應(yīng)用內(nèi)通知)發(fā)送給用戶。
通過將通知服務(wù)與實際發(fā)送解耦,渠道處理器實現(xiàn)了獨立擴展和異步處理通知。
這種設(shè)置允許每個處理器專注于其指定的渠道,確??煽康陌l(fā)送,并內(nèi)置重試機制和高效處理故障。
(6) 數(shù)據(jù)庫/存儲
數(shù)據(jù)庫/存儲層管理大量數(shù)據(jù),包括通知內(nèi)容、用戶偏好、定時通知、發(fā)送日志和元數(shù)據(jù)。
系統(tǒng)需要混合存儲解決方案來支持不同需求:
- 事務(wù)性數(shù)據(jù):使用關(guān)系數(shù)據(jù)庫(如 PostgreSQL或 MySQL)存儲結(jié)構(gòu)化數(shù)據(jù),如通知日志和發(fā)送狀態(tài)。
 - 用戶偏好:使用NoSQL數(shù)據(jù)庫(如 MongoDB)存儲大量用戶特定數(shù)據(jù),如偏好和限速。
 - Blob存儲:對于包含大附件的通知(如帶圖片或 PDF的電子郵件),使用 OSS,Amazon S3或類似服務(wù)存儲這些附件。
 
Low-level設(shè)計
設(shè)計完 High-level,我們將進入更詳細(xì)的 Low-level 設(shè)計層面,主要包含以下步驟:
步驟1:通知請求創(chuàng)建
首先,通知系統(tǒng)的調(diào)用方(如電商平臺、或營銷系統(tǒng)等)需要生成通知請求。
請求的消息結(jié)構(gòu)如示例請求:
{
  "requestId": "xxx1",
  "timestamp": "2024-09-18T22:00:00Z",
  "notificationType": "transactional",
  "channels": ["email", "sms", "push"],
  "recipient": {
    "userId": "user1",
    "email": "user1@example.com"
  },
  "message": {
    // 消息體
  }
}步驟2:通知服務(wù)接收
當(dāng)調(diào)用方發(fā)出請求后,通知服務(wù)(通過API網(wǎng)關(guān)/負(fù)載均衡器)會接收到通知請求。請求經(jīng)過身份驗證和驗證,確保其來自授權(quán)來源,并包含所有必要信息(接收者、消息、渠道等)。
步驟3:獲取用戶偏好
通知服務(wù)會查詢用戶的一些偏好服務(wù),這部分帶有一些定制化的功能,可以根據(jù)實際情況決定是否需要此部分:
- 偏好的通知渠道(如某些用戶可能偏好通過電子郵件接收促銷消息,但通過短信接收關(guān)鍵警報)。
 - 選擇加入/退出偏好:確保符合用戶偏好,如用戶選擇退出營銷郵件。
 - 限速:確保用戶沒有超過其配置的通知限制(如每天最多3條促銷短信)。
 
步驟4:定時發(fā)送
如果通知計劃需要在未來的某個時刻(例如:每分鐘或基于更細(xì)粒度的間隔))發(fā)送,通知服務(wù)將通知發(fā)送到調(diào)度服務(wù),后者將通知及其預(yù)定發(fā)送時間存儲在基于時間的數(shù)據(jù)庫或允許基于時間高效查詢的 NoSQL數(shù)據(jù)庫中。
調(diào)度服務(wù)需要定時功能,當(dāng)?shù)竭_預(yù)定時間時,調(diào)度服務(wù)將通知發(fā)送到通知隊列。
步驟5:將通知放入隊列
一旦通知服務(wù)創(chuàng)建并格式化了所需渠道的消息,它將每個消息放入通知隊列系統(tǒng)中的相應(yīng)主題(如Kafka、RocketMQ等)。
每個渠道(電子郵件、短信、推送等)都有自己的專用主題,確保消息由相關(guān)的渠道處理器獨立處理。
例如:如果通知需要通過電子郵件、短信和推送發(fā)送,通知服務(wù)將生成三條消息,每條消息都針對相應(yīng)的渠道進行定制。
- 電子郵件消息放入電子郵件主題。
 - 短信消息放入短信主題。
 - 推送通知消息放入推送主題。
 
這些主題允許每個渠道處理器專注于消費其相關(guān)的消息,減少復(fù)雜性并提高處理效率。
每條消息包含通知負(fù)載、渠道特定信息和元數(shù)據(jù)(如優(yōu)先級和重試計數(shù))。
步驟6:渠道特定的消息處理
通知隊列存儲消息,直到相關(guān)的渠道處理器拉取它們進行處理。
每個渠道處理器作為隊列的消費者,負(fù)責(zé)消費自己的消息:
- 電子郵件處理器從電子郵件主題拉取消息。
 - 短信處理器從短信主題拉取消息。
 - 推送處理器從推送主題拉取消息。
 - 應(yīng)用內(nèi)處理器從應(yīng)用內(nèi)主題拉取消息。
 
步驟7:發(fā)送通知
每個渠道處理器負(fù)責(zé)通過指定的渠道發(fā)送通知:
電子郵件處理器:
- 連接到電子郵件提供商(如SendGrid、Mailgun、Amazon SES)。
 - 發(fā)送電子郵件,確保其符合用戶偏好(如HTML或純文本)。
 - 處理錯誤如退信或無效的電子郵件地址。
 
短信處理器:
- 連接到短信提供商(如Twilio、Nexmo)。
 - 發(fā)送短信,并進行任何格式調(diào)整以滿足字符限制或區(qū)域要求。
 - 處理問題如無效的電話號碼或網(wǎng)絡(luò)錯誤。
 
推送通知處理器:
- 使用服務(wù)如Firebase Cloud Messaging(FCM)用于Android或Apple Push Notification Service(APNs)用于iOS。
 - 發(fā)送推送通知,包括任何元數(shù)據(jù)(如應(yīng)用程序特定的操作或圖標(biāo))。
 - 處理失敗如過期的設(shè)備令牌或離線設(shè)備。
 
應(yīng)用內(nèi)通知處理器:
- 通過WebSockets或長輪詢將應(yīng)用內(nèi)通知發(fā)送到用戶的活動會話。
 - 格式化消息以在應(yīng)用程序的UI中顯示,遵循任何應(yīng)用程序特定的顯示規(guī)則。
 
步驟8:監(jiān)控和發(fā)送確認(rèn)
每個渠道處理器等待來自外部提供商的確認(rèn):
- 成功:消息已發(fā)送。
 - 失敗:消息發(fā)送失?。ㄈ缇W(wǎng)絡(luò)問題、無效地址)。
 
渠道處理器將每條通知的狀態(tài)記錄在通知日志表中,以供將來參考、審核和報告。
關(guān)鍵問題和瓶頸
1.故障和重試
如果通知發(fā)送由于臨時問題(如第三方提供商停機)而失敗,渠道處理器將嘗試重發(fā)通知。
- 通常使用指數(shù)退避策略,每次重試的延遲時間逐漸增加。
 - 如果通知在設(shè)定次數(shù)的重試后仍未發(fā)送成功,則將其移動到死信隊列(DLQ)以進一步處理。
 - 管理員可以手動審核和重新處理死信隊列中的消息。
 
2.可擴展性
(1) 水平擴展
系統(tǒng)應(yīng)設(shè)計為水平擴展,意味著組件可以通過增加實例來應(yīng)對負(fù)載增加。
- 通知服務(wù):隨著請求量的增加,可以部署更多實例來管理增加的通知請求量。
 - 通知隊列:分布式隊列系統(tǒng)(如Kafka或RabbitMQ)天然具有可擴展性,可以通過將隊列分布在多個節(jié)點上來處理更大的工作量。
 - 渠道處理器:每個處理器(電子郵件、短信等)應(yīng)水平擴展以處理大量通知。
 
(2) 分片和分區(qū)
為了高效處理大量數(shù)據(jù),特別是用戶數(shù)據(jù)和通知日志,分片和分區(qū)將負(fù)載分布在多個數(shù)據(jù)庫或地理區(qū)域:
- 基于用戶的分片:根據(jù)地理位置或用戶ID將用戶分布在不同的數(shù)據(jù)庫或區(qū)域,以平衡負(fù)載。
 - 基于時間的分區(qū):將通知日志組織成基于時間的分區(qū)(如每日或每月),以提高查詢性能并管理大量歷史數(shù)據(jù)。
 
(3) 緩存
使用Redis或Memcached等解決方案實現(xiàn)緩存,以存儲頻繁訪問的數(shù)據(jù),如用戶偏好。
緩存減少數(shù)據(jù)庫負(fù)載,并通過避免重復(fù)的數(shù)據(jù)庫查詢來提高實時通知的響應(yīng)時間。
3.可靠性
為了高可用性,數(shù)據(jù)(如用戶偏好、日志)應(yīng)在多個數(shù)據(jù)中心或區(qū)域之間復(fù)制。這確保即使一個區(qū)域故障,數(shù)據(jù)在其他地方仍然可用。
多AZ復(fù)制:在多個可用區(qū)存儲數(shù)據(jù),以提供冗余。
使用負(fù)載均衡器將傳入流量均勻分布在通知服務(wù)的各個實例之間,確保沒有單個實例成為瓶頸。
4.監(jiān)控和日志記錄
為了確保系統(tǒng)在大規(guī)模下的平穩(wěn)運行,系統(tǒng)應(yīng)具備:
- 集中式日志記錄:使用ELK Stack或Prometheus/Grafana等工具收集各種組件的日志并監(jiān)控系統(tǒng)健康。
 - 警報:設(shè)置警報以監(jiān)控故障(如通知發(fā)送失敗率超過閾值)。
 - 指標(biāo):跟蹤每個渠道的成功率、失敗率、發(fā)送延遲和吞吐量等指標(biāo)。
 
5.安全性
對所有傳入通知服務(wù)的請求實施強認(rèn)證(如OAuth 2.0)。使用基于角色的訪問控制(RBAC)限制對關(guān)鍵服務(wù)的訪問。
通過在API網(wǎng)關(guān)上實施速率限制保護服務(wù)免受濫用,防止DoS攻擊。
6.歸檔舊數(shù)據(jù)
由于通知系統(tǒng)隨著時間的推移會處理大量數(shù)據(jù),實施歸檔舊數(shù)據(jù)的策略非常重要。
歸檔涉及將過時或不常訪問的數(shù)據(jù)(如舊的發(fā)送日志、通知內(nèi)容和用戶歷史記錄)從主存儲移動到成本較低、長期存儲解決方案。
這樣可以減少主存儲的負(fù)載并提高系統(tǒng)的整體性能。
總結(jié)
這篇文章,我們從需求分析出發(fā),再到宏觀層面的設(shè)計,最后到詳細(xì)的設(shè)計,通過本文詳細(xì)地分析了,我們不僅能夠?qū)W到如何設(shè)計一個可擴展的通知服務(wù),同時我們還能通過通知服務(wù)的設(shè)計更好去理解系統(tǒng)設(shè)計的思路。















 
 
 


 
 
 
 