2系统设计面试:内幕指南(第2版)

近实时消息传递系统

案例1

消息系统长连接消息存储

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第2章:近实时消息传递系统(Near Real-Time Messaging System)

一、导读

1.1 本章学习目标

第2章以设计类似微信/WhatsApp 的消息系统为案例,完整演示系统设计面试的流程。核心挑战包括消息实时推送、海量消息存储、消息可靠性保证、群聊设计等。

本章的核心学习目标包括:

  • 掌握长连接方案选型:WebSocket vs 长轮询 vs SSE 的权衡。
  • 理解消息存储设计:消息表 Schema、索引策略、分库分表。
  • 掌握消息投递模型:推(Push)vs 拉(Pull)vs 推拉结合。
  • 理解消息可靠性保障:消息 ACK、重试机制、去重。
  • 学会群聊设计:Fan-out on write vs Fan-out on read 的权衡。
  • 了解消息排序方案:逻辑时钟 vs 物理时钟。

1.2 为什么本章如此重要

消息系统是系统设计面试中出现频率最高的题目之一。它涵盖了系统设计的多个核心领域:长连接管理、消息存储、实时推送、分布式一致性。Alex Xu 通过这个案例展示了如何处理高并发、低延迟、高可靠的实时系统。

1.3 核心问题引导

WebSocket 和长轮询的区别是什么?如何选择?

消息存储应该用关系型数据库还是 NoSQL?

推模型和拉模型各有什么优缺点?

如何保证消息不丢失、不重复、按序到达?

10人群和1000人群的架构设计有何不同?

如何处理消息的已读状态?


二、核心概念详解

2.1 需求分析与规模估算

// 功能需求:
// 1. 一对一私聊:发送和接收文本、图片、视频消息
// 2. 群聊:支持最多100人的群组
// 3. 在线状态:显示联系人是否在线
// 4. 消息历史:可以查看历史消息
// 5. 消息通知:离线消息推送

// 非功能需求:
// 1. 低延迟:消息发送后 < 200ms 送达
// 2. 高可用:99.99%
// 3. DAU:5000万
// 4. 每日消息量:10亿条

// 规模估算:
// 每日消息量:10亿条
// 每条消息大小:文本(200B) + 元数据(100B) = 300B
// 每日存储增量:10亿 × 300B = 300GB
// 每月存储增量:300GB × 30 = 9TB
// 峰值 QPS:10亿 / 86400 × 3 ≈ 35,000 QPS
// 长连接数:5000万 × 20%(同时在线)= 1000万

2.2 高层设计

// 系统架构:
//
// [Client] ← WebSocket → [WebSocket Server × N]
//                              ↓
//                         [Message Service]
//                           ↓    ↓    ↓
//                     [MySQL] [Redis] [Kafka]
//                     (持久化) (在线状态) (异步处理)
//                              ↓
//                        [Notification Service]
//                              ↓
//                        [Push Service (APNs/FCM)]

// 核心组件:
// 1. WebSocket Server:管理客户端长连接
// 2. Message Service:消息的收发、存储、路由
// 3. MySQL:消息持久化存储
// 4. Redis:在线状态、未读消息计数
// 5. Kafka:异步消息处理(通知、推送)
// 6. Notification Service:离线推送

2.3 长连接管理

2.3.1 WebSocket vs 长轮询

// WebSocket(推荐):
// 客户端和服务端建立一次 HTTP 握手后,升级为双向通信协议
// 优点:真正的双向通信,延迟最低,开销最小
// 缺点:需要支持 WebSocket 的服务器,中间代理可能不支持
// 适用:现代消息系统(微信、WhatsApp、Slack)

// 长轮询(Long Polling):
// 客户端发送请求,服务端保持连接直到有新消息或超时
// 优点:兼容性好,不需要 WebSocket 支持
// 缺点:每次获取消息需要新建连接,开销大
// 适用:兼容性要求高的场景

// SSE(Server-Sent Events):
// 服务端单向推送,客户端通过 HTTP 接收
// 优点:简单,基于 HTTP
// 缺点:只能服务端→客户端,不支持双向
// 适用:通知推送、股票行情等单向推送场景

// 结论:消息系统选择 WebSocket

2.3.2 WebSocket 连接管理

// 连接管理挑战:
// 1000万长连接如何管理?
//
// 方案:分布式 WebSocket Server
// [Client1] → [WS Server 1] ←→ [Redis Pub/Sub]
// [Client2] → [WS Server 1]
// [Client3] → [WS Server 2] ←→ [Redis Pub/Sub]
// [Client4] → [WS Server 2]
//
// 问题:Client1 发消息给 Client3(在不同 WS Server 上)
// 解决:通过 Redis Pub/Sub 跨 Server 推送
//
// WS Server 1 收到 Client1 的消息
// → 发布到 Redis Channel "user:client3"
// → WS Server 2 订阅了该 Channel
// → WS Server 2 推送给 Client3

// 连接状态维护:
// 1. 心跳检测:客户端每30秒发送 Ping,服务端回复 Pong
// 2. 超时断开:60秒未收到心跳,断开连接
// 3. 断线重连:客户端自动重连,指数退避

2.4 消息存储设计

2.4.1 数据库选型

// 方案1:关系型数据库(MySQL/PostgreSQL)
// 优点:事务支持好,查询灵活
// 缺点:写入吞吐量有限,分库分表复杂
// 适用:消息量中等,需要复杂查询

// 方案2:NoSQL(Cassandra/HBase)
// 优点:写入吞吐量极高,水平扩展容易
// 缺点:查询能力有限,不支持事务
// 适用:消息量大,写入密集

// 方案3:混合方案(推荐)
// 最近消息 → Redis(缓存,最近100条)
// 历史消息 → Cassandra(持久化,海量存储)
// 元数据   → MySQL(用户、群组信息)

// WhatsApp 的选择:
// 使用自定义存储引擎,基于 SSD
// 消息按对话分片,每个对话的数据在同一节点

2.4.2 Schema 设计

sql
-- 方案1:消息表(适合私聊)
CREATE TABLE messages (
    message_id BIGINT PRIMARY KEY,
    sender_id BIGINT NOT NULL,
    receiver_id BIGINT NOT NULL,
    content TEXT,
    message_type ENUM('text', 'image', 'video', 'file'),
    status ENUM('sent', 'delivered', 'read') DEFAULT 'sent',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_receiver_time (receiver_id, created_at DESC),
    INDEX idx_sender_receiver (sender_id, receiver_id, created_at)
);

-- 方案2:会话 + 消息表(适合群聊,推荐)
CREATE TABLE conversations (
    conversation_id BIGINT PRIMARY KEY,
    type ENUM('private', 'group') NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_message_at TIMESTAMP,
    last_message_id BIGINT
);

CREATE TABLE conversation_members (
    conversation_id BIGINT,
    user_id BIGINT,
    role ENUM('owner', 'admin', 'member') DEFAULT 'member',
    joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_read_message_id BIGINT DEFAULT 0,
    PRIMARY KEY (conversation_id, user_id),
    INDEX idx_user (user_id)
);

CREATE TABLE messages (
    message_id BIGINT PRIMARY KEY,
    conversation_id BIGINT NOT NULL,
    sender_id BIGINT NOT NULL,
    content TEXT,
    message_type ENUM('text', 'image', 'video', 'file') DEFAULT 'text',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_conv_time (conversation_id, created_at DESC)
);

2.5 消息投递模型

2.5.1 Push vs Pull

// Push(推)模型:
// 服务端主动推送消息给接收者
// 优点:实时性好
// 缺点:
//   - 接收者离线时需要存储推送队列
//   - 群聊中需要为每个成员推送(Fan-out on write)
//   - 1000人群发消息,写放大1000倍

// Pull(拉)模型:
// 接收者主动拉取新消息
// 优点:
//   - 实现简单
//   - 群聊无写放大
// 缺点:
//   - 实时性差(需要轮询)
//   - 无效拉取多(轮询时可能没有新消息)

// Push + Pull(推荐):
// 在线时:服务端通过 WebSocket 推送
// 离线时:上线后拉取离线期间的消息
//
// 流程:
// 1. 用户A发送消息 → 写入 DB → 推送给用户B(如果在线)
// 2. 用户B离线 → 消息写入 DB,标记为未推送
// 3. 用户B上线 → 拉取 last_read_message_id 之后的消息
// 4. 后续新消息通过 WebSocket 实时推送

2.5.2 群聊的 Fan-out 策略

// Fan-out on Write(写时扩散):
// 发送一条群消息 → 为每个群成员写入一条记录
// 优点:读取简单(只查自己的消息列表)
// 缺点:写放大严重(1000人群写1000次)
// 适用:小群(< 100人)

// Fan-out on Read(读时扩散):
// 发送一条群消息 → 只写入群消息表一次
// 读取时:查询群消息表,合并所有群的消息
// 优点:无写放大
// 缺点:读取复杂(需要合并多个群的消息)
// 适用:大群(> 100人)

// 混合策略(推荐):
// 小群(< 100人)→ Fan-out on Write
// 大群(≥ 100人)→ Fan-out on Read
//
// 实现:
// conversations 表中记录群类型
// 小群消息写入每个成员的消息收件箱
// 大群消息只写入群消息表,成员拉取时查询

2.6 消息可靠性

// 消息投递的三个级别:
// At-most-once:消息可能丢失(fire and forget)
// At-least-once:消息不丢失,可能重复
// Exactly-once:消息不丢失不重复(最难实现)

// 消息系统的实现:At-least-once + 客户端去重
//
// 发送端:
// 1. 客户端生成唯一 message_id
// 2. 发送消息到服务端
// 3. 等待服务端 ACK
// 4. 超时未收到 ACK → 重发(携带相同 message_id)
//
// 服务端:
// 1. 收到消息,检查 message_id 是否已处理
// 2. 如果已处理 → 返回 ACK(不重复写入)
// 3. 如果未处理 → 写入 DB → 返回 ACK
//
// 接收端:
// 1. 收到消息,检查 message_id 是否已处理
// 2. 如果已处理 → 丢弃(去重)
// 3. 如果未处理 → 渲染消息 → 发送 ACK

三、实战案例

3.1 案例一:消息发送的完整流程

// 用户A给用户B发送文本消息"Hello":
//
// 1. [Client A] 生成 message_id = msg-001
// 2. [Client A] 通过 WebSocket 发送消息到 WS Server
//    { message_id: "msg-001", to: "userB", content: "Hello", type: "text" }
// 3. [WS Server] 转发到 Message Service
// 4. [Message Service] 写入数据库
//    INSERT INTO messages (message_id, sender_id, receiver_id, content, status)
//    VALUES ('msg-001', userA, userB, 'Hello', 'sent')
// 5. [Message Service] 检查 userB 是否在线
//    → 查询 Redis:userB 的在线状态和所在 WS Server
// 6. [Message Service] 如果 userB 在线:
//    → 通过 Redis Pub/Sub 通知对应的 WS Server
//    → WS Server 推送消息给 Client B
// 7. [Client B] 收到消息,发送 ACK
// 8. [Message Service] 更新消息状态为 'delivered'
// 9. [Client A] 收到 delivered 通知,显示 "已送达"
// 10. [Client B] 打开对话,消息标记为 'read'
// 11. [Message Service] 更新状态为 'read'
// 12. [Client A] 收到 read 通知,显示 "已读"

3.2 案例二:群聊消息分发

// 用户A在100人群(conversation_id=100)发送消息:
//
// 方案1:Fan-out on Write(小群)
// 1. 写入群消息表
// 2. 为100个成员各写入一条消息到收件箱
// 3. 在线成员通过 WebSocket 实时推送
// 4. 离线成员上线后拉取
//
// 方案2:Fan-out on Read(大群,如1000人群)
// 1. 只写入群消息表(1次写入)
// 2. 在线成员通过 WebSocket 实时推送
// 3. 成员打开群聊时,查询群消息表获取最新消息
// 4. 维护每个成员的 last_read_message_id

四、本章小结

核心知识点:

  • WebSocket 是消息系统的最佳长连接方案。
  • 消息存储推荐混合方案:Redis(缓存)+ Cassandra(持久化)+ MySQL(元数据)。
  • 消息投递采用 Push + Pull 混合模型:在线推送,离线拉取。
  • 群聊设计:小群 Fan-out on Write,大群 Fan-out on Read。
  • 消息可靠性通过 At-least-once + 客户端去重实现。

五、思考与练习

思考题

如果 WebSocket Server 宕机,已建立连接的客户端如何处理?

消息的"已读"状态在高并发下如何保证性能?

如何设计消息的撤回功能?

练习题

设计一个支持私聊和群聊的消息系统,画出完整架构图。

设计消息表的 Schema,支持文本、图片、视频消息。

设计群聊消息的分发策略,对比10人群和10000人群的区别。

2/6近实时消
0%