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