引言
在互聯(lián)網(wǎng)應(yīng)用中,站內(nèi)未讀消息系統(tǒng)是維系用戶活躍度與粘性的核心功能之一。當(dāng)面對如“享讀系統(tǒng)”這樣需要支撐50萬Qps(每秒查詢量)的高并發(fā)場景時,傳統(tǒng)的數(shù)據(jù)庫直接讀寫方案會迅速成為性能瓶頸。本文將系統(tǒng)性地探討如何設(shè)計一個高可用、低延遲、可擴(kuò)展的站內(nèi)未讀消息系統(tǒng),以應(yīng)對海量實時請求的挑戰(zhàn)。
一、 核心挑戰(zhàn)與設(shè)計目標(biāo)
在50萬Qps的壓力下,系統(tǒng)設(shè)計面臨多重挑戰(zhàn):
- 極致性能與低延遲:用戶對未讀數(shù)的感知要求幾乎是實時的,讀取延遲必須控制在毫秒級。
- 高并發(fā)寫入:用戶每閱讀一條消息、系統(tǒng)每推送一條新消息,都可能觸發(fā)未讀數(shù)的更新,寫入并發(fā)量巨大。
- 數(shù)據(jù)一致性:在分布式環(huán)境下,保證用戶看到的未讀計數(shù)準(zhǔn)確無誤,避免出現(xiàn)多讀或少讀。
- 可擴(kuò)展性與高可用:系統(tǒng)需能隨著用戶量增長平滑擴(kuò)容,且任何單點故障不應(yīng)影響核心服務(wù)。
因此,我們的設(shè)計目標(biāo)聚焦于:讀擴(kuò)散、異步化、內(nèi)存優(yōu)先、最終一致性。
二、 架構(gòu)設(shè)計總覽
整體架構(gòu)采用分層、分模塊的設(shè)計思想,核心分為三層:
- 接入層:采用高性能網(wǎng)關(guān)(如Nginx、API Gateway)進(jìn)行負(fù)載均衡與請求路由,并實現(xiàn)限流、熔斷等保護(hù)措施。
- 邏輯服務(wù)層:
- 消息投遞服務(wù):負(fù)責(zé)處理新消息的創(chuàng)建與分發(fā)邏輯,將“有新消息”這個事件異步通知給計數(shù)服務(wù)。
- 未讀計數(shù)服務(wù):系統(tǒng)的核心,負(fù)責(zé)未讀計數(shù)的增、刪、改、查。它不直接處理業(yè)務(wù)邏輯,而是作為計數(shù)緩存的管理者。
- 會話/列表服務(wù):負(fù)責(zé)管理用戶的消息會話列表和消息內(nèi)容本身,與計數(shù)服務(wù)解耦。
- 數(shù)據(jù)存儲層:采用多級混合存儲策略。
三、 核心設(shè)計策略
1. 讀寫分離與讀擴(kuò)散
放棄為每條消息單獨標(biāo)記已讀/未讀的“寫擴(kuò)散”模式(存儲開銷和寫入壓力巨大)。采用 “讀擴(kuò)散” 模式:
- 未讀集存儲:系統(tǒng)只為每個用戶維護(hù)一個未讀消息ID的集合(或一個總計數(shù))。
- 判定邏輯:當(dāng)用戶查詢某個會話的未讀數(shù)時,由服務(wù)端實時計算:該會話的最新消息ID與用戶已讀的最后一條消息ID之間的差值(或檢查消息ID是否在用戶的未讀集合中)。這將對數(shù)據(jù)庫的頻繁寫入轉(zhuǎn)移為對緩存的高效讀取。
2. 緩存優(yōu)先與存儲選型
- 一級緩存(熱點數(shù)據(jù)):使用 Redis 集群作為核心計數(shù)存儲。
- 存儲結(jié)構(gòu):為每個用戶維護(hù)一個
Hash,鍵為 uid,字段為會話ID(sid),值為該會話的未讀數(shù)??梢栽O(shè)置一個總未讀數(shù)的字段。
- 優(yōu)勢:內(nèi)存讀寫,性能極高;豐富的數(shù)據(jù)結(jié)構(gòu)(Hash, Sorted Set)能很好支撐聚合查詢和范圍查詢。
- 容量規(guī)劃:以2億用戶,每個用戶平均10個活躍會話估算,存儲量可控,可通過集群分片(如Codis, Redis Cluster)輕松擴(kuò)展。
- 二級備份與持久化:使用 Apache Cassandra 或 TiDB 等分布式數(shù)據(jù)庫。
- 作用:持久化存儲全量的用戶-會話未讀關(guān)系,作為Redis數(shù)據(jù)的備份和恢復(fù)源。
- 選型理由:它們具有高寫入吞吐、線性擴(kuò)展能力,適合海量數(shù)據(jù)的最終一致性存儲。
- 消息同步:采用 異步雙寫 或 Write-Through 策略。所有計數(shù)更新先寫入Redis,確保前端響應(yīng)速度;隨后通過消息隊列(如Apache Kafka, RocketMQ)異步同步到分布式數(shù)據(jù)庫,實現(xiàn)最終一致性。
3. 計數(shù)更新策略——增量與合并
直接為每條新消息實時更新全局計數(shù)會給Redis帶來巨大壓力。優(yōu)化方案:
- 本地累加:在消息投遞服務(wù)中,為每個用戶維護(hù)一個小的內(nèi)存累加器(如Guava Cache),在短時間內(nèi)(如1秒)將多次增量合并為一次更新操作,再批量寫入Redis。這能極大減少對Redis的網(wǎng)絡(luò)請求和寫入命令。
- 延遲更新:對于非強(qiáng)實時性的全局總未讀數(shù),可以接受秒級的延遲更新,通過后臺任務(wù)定期從各會話計數(shù)聚合計算。
4. 容災(zāi)與數(shù)據(jù)一致性保障
- Redis數(shù)據(jù)持久化與備份:開啟AOF和RDB,結(jié)合哨兵或集群模式實現(xiàn)高可用。
- 兜底查詢:當(dāng)Redis集群發(fā)生故障或緩存未命中時,服務(wù)應(yīng)能自動降級,從分布式數(shù)據(jù)庫中查詢并回種緩存。為防止緩存擊穿,需使用分布式鎖或布隆過濾器。
- 最終一致性核對:設(shè)立定時對賬任務(wù),比對Redis與分布式數(shù)據(jù)庫中的計數(shù)差異,并進(jìn)行修復(fù),確保數(shù)據(jù)長期準(zhǔn)確。
四、 關(guān)鍵流程示例
- 用戶查詢未讀數(shù)(讀流程):
- 請求到達(dá)網(wǎng)關(guān),路由至未讀計數(shù)服務(wù)。
- 服務(wù)直接查詢Redis集群中對應(yīng)用戶的Hash結(jié)構(gòu),獲取各會話未讀數(shù)及總數(shù)。
- 新消息到達(dá)(寫流程):
- 向Kafka發(fā)送一個事件:
{"uid": 123, "sid": 456, "increment": 1}。
- 未讀計數(shù)服務(wù)消費該事件,先在本地累加器合并增量。
- 累加器定時(如每秒)將合并后的增量(例如,
{123: {456: 5}} 表示用戶123在會話456的未讀數(shù)需增加5),通過 HINCRBY 命令批量更新至Redis。
- 另一個消費者將同樣的更新異步寫入Cassandra進(jìn)行持久化。
五、 性能估算與優(yōu)化
- Redis集群估算:假設(shè)50萬Qps全部為讀請求,單Redis實例(分片)處理約8-10萬Qps,則需要約5-7個分片組成的集群。實際中讀寫混合,需根據(jù)比例增加資源。通過Pipeline和批量命令可進(jìn)一步提升吞吐。
- 網(wǎng)絡(luò)與序列化:使用高性能序列化協(xié)議(如Protobuf),優(yōu)化網(wǎng)關(guān)與服務(wù)間的網(wǎng)絡(luò)通信。
- 監(jiān)控與調(diào)優(yōu):建立完善的監(jiān)控體系(如Prometheus + Grafana),實時跟蹤Qps、延遲、緩存命中率、數(shù)據(jù)庫負(fù)載等核心指標(biāo),以便動態(tài)調(diào)整和擴(kuò)容。
結(jié)論
設(shè)計一個支持50萬Qps的站內(nèi)未讀消息系統(tǒng),關(guān)鍵在于將核心的計數(shù)功能從傳統(tǒng)數(shù)據(jù)庫中剝離,構(gòu)建一個以內(nèi)存緩存(Redis)為核心、異步持久化為保障的獨立服務(wù)體系。通過采用“讀擴(kuò)散”、增量合并、多級緩存、最終一致性等設(shè)計模式,能夠有效化解高并發(fā)壓力,實現(xiàn)低延遲、高可用的目標(biāo)。享讀系統(tǒng)的實踐表明,清晰的分層架構(gòu)和針對性的技術(shù)選型,是應(yīng)對此類規(guī)模挑戰(zhàn)的堅實基礎(chǔ)。系統(tǒng)上線后,仍需持續(xù)監(jiān)控和迭代,以應(yīng)對未來可能增長的業(yè)務(wù)量。