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