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