系统设计 Deep Dive:设计实时聊天系统
系统设计深度解析:设计实时聊天系统。涵盖WebSocket协议、消息队列、消息持久化与离线推送架构方案,还原真实系统设计面试的完整思考过程、技术选型权衡与面试官追问方向。
面试官真实提问
“请设计一个实时聊天系统,类似 WhatsApp 或微信。支持单聊和群聊,消息需要保证送达,用户离线时消息需要保存。”
“如何处理消息的顺序一致性?如果用户同时在手机和电脑上登录,消息如何同步?”
这道题考察实时通信、消息队列、数据一致性和多设备同步的综合设计能力,在 Meta、Google、Amazon 的面试中频繁出现。
需求澄清清单
功能需求
Must-have:
- 单聊(一对一实时消息)
- 群聊(多人消息广播)
- 消息持久化
- 离线消息(用户上线后收到离线期间的消息)
- 已读/未读状态
Nice-to-have:
- 媒体消息(图片、视频、文件)
- 消息撤回
- 端到端加密
- 消息搜索
规模估算
| 指标 | 估算值 |
|---|---|
| MAU | 10 亿 |
| DAU | 5 亿 |
| 每日消息 | 500 亿条 |
| 平均会话数 | 每个用户 50 个活跃会话 |
| 消息延迟 | P99 < 1 秒 |
非功能需求
- 可用性: 99.99%(消息不能丢)
- 一致性: 消息顺序一致性(同一会话内)
- 安全性: 端到端加密
第 1 步:高层设计
┌─────────┐
│ 客户端 │ WebSocket
└────┬────┘
│
┌────▼────────────┐
│ WebSocket Gateway│
└────┬────────────┘
│
┌─┴─┐
┌──▼──┐ ┌──▼──┐
│ Chat│ │ Chat│ Node A / B
│ A │ │ B │
└──┬──┘ └──┬──┘
└──┬──┘
┌──▼──────┐
│ Kafka │ Message Queue
└──┬──────┘
│
┌─┴─┐
┌───▼┐ ┌───▼──────┐
│Msg │ │ Presence │
│Store│ │ Service │ Cassandra / Redis
└────┘ └──────────┘
核心组件:
- WebSocket Gateway:管理所有客户端的长连接
- Chat Server:处理消息路由、投递、状态更新
- Kafka:消息持久化、异步处理、服务解耦
- Message Store:持久化存储所有消息
- Presence Service:在线状态管理
第 2 步:核心组件设计
API 设计
WebSocket 协议:
// 客户端 → 服务器
{
"type": "SEND_MESSAGE",
"session_id": "session_123",
"to_user_id": "user_456",
"content": "Hello!",
"timestamp": 1651785600000
}
// 服务器 → 客户端
{
"type": "MESSAGE_RECEIVED",
"session_id": "session_123",
"from_user_id": "user_456",
"content": "Hello!",
"timestamp": 1651785600000,
"message_id": "msg_789"
}
// 已读回执
{
"type": "READ_RECEIPT",
"session_id": "session_123",
"message_id": "msg_789"
}
REST API(辅助):
# 获取历史消息
GET /api/v1/messages?session_id=123&before=456&limit=50
# 获取未读数
GET /api/v1/unread-count?user_id=123
数据模型
-- 会话表
CREATE TABLE sessions (
session_id BIGINT PRIMARY KEY,
type ENUM('DM', 'GROUP'),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
-- 会话成员
CREATE TABLE session_members (
session_id BIGINT,
user_id BIGINT,
joined_at TIMESTAMP,
last_read_msg_id BIGINT,
PRIMARY KEY (session_id, user_id)
);
-- 消息表
CREATE TABLE messages (
message_id BIGINT PRIMARY KEY,
session_id BIGINT,
sender_id BIGINT,
content TEXT,
message_type ENUM('TEXT', 'IMAGE', 'VIDEO', 'FILE'),
created_at TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE,
INDEX idx_session_created (session_id, created_at)
);
-- 消息状态
CREATE TABLE message_status (
message_id BIGINT,
user_id BIGINT,
status ENUM('SENT', 'DELIVERED', 'READ'),
updated_at TIMESTAMP,
PRIMARY KEY (message_id, user_id)
);
核心考点:WebSocket 连接管理
连接生命周期:
连接建立 → 认证 → 注册路由 → 心跳保活 → 消息收发 → 断开清理
Sticky Sessions(粘滞会话):
客户端 A → WebSocket Gateway → Chat Server Node A(固定绑定)
客户端 B → WebSocket Gateway → Chat Server Node B(固定绑定)
好处:服务端知道每个客户端在哪个节点
问题:单点故障、负载不均
连接路由表:
Redis Hash:
connection:{user_id} → {
"node_id": "node_A",
"connection_id": "conn_123",
"last_heartbeat": 1651785600
}
消息路由与投递
单聊消息投递流程:
1. 发送方 A 发送消息 → WebSocket Gateway → Chat Server
2. Chat Server 写入 Kafka(持久化)
3. Chat Server 查询接收方 B 的连接状态
4. 如果 B 在线 → 直接推送到 B 的 WebSocket 连接
5. 如果 B 离线 → 存储到 Message Store,B 上线时拉取
6. 更新消息状态(SENT → DELIVERED)
群聊消息投递流程:
1. 发送方发送消息 → Chat Server
2. Chat Server 写入 Kafka
3. 查询群成员在线状态
4. 批量推送到在线成员
5. 离线成员的消息存储到 Message Store
消息投递保障:
At least once(至少一次):
- 发送方:消息未收到 ACK → 重试
- 接收方:需要去重(message_id 唯一索引)
Exactly once(精确一次):
- 需要分布式事务,复杂度高
- 实际系统多用 at-least-once + 幂等处理
离线消息处理
离线消息存储:
用户离线期间,消息存储到 Message Store
用户上线时:
1. 拉取离线消息(最近 N 条或从 last_read_msg_id 开始)
2. 标记为 DELIVERED
3. 删除已读的离线消息(可选,节省存储)
离线消息过期策略:
- 在线用户:不保留离线消息
- 离线 < 7 天:保留所有离线消息
- 离线 > 7 天:只保留最近 1000 条
已读/未读状态
发送方:
- 单勾(✓):已发送
- 双勾(✓✓):已送达
- 蓝色双勾(✓✓):已读
实现:
1. 接收方打开聊天窗口 → 拉取未读消息
2. 标记所有消息为 READ
3. 发送 READ_RECEIPT 给发送方
4. 发送方的 Chat Server 推送状态更新
第 3 步:扩展性与优化
重点 水平扩展
问题:WebSocket 连接的 Sticky Session 导致扩展困难
方案 1:Pub/Sub 模式
Chat Server A ──────┐
├──→ Kafka (messages topic) ───→ Chat Server B
│ 消息路由 │
Chat Server B ──────┘ │
▼
所有 Chat Server 订阅 Kafka
根据消息的目标用户路由
方案 2:连接服务分离
┌── 连接服务分离 ──────────────────┐
│ │
│ Connection Service │
│ (WebSocket) │
│ │ │
│ │ 消息转发 │
│ ▼ │
│ Message Router │
│ (Kafka-based) │
│ │
└──────────────────────────────────┘
Connection Service:只管理 WebSocket 连接
Message Router:基于 Kafka 的消息路由
群聊设计
大群聊(1000+ 人)的挑战:
- 消息广播成本高(一条消息 → 1000 次推送)
- 消息同步复杂(1000 人的已读状态)
解决方案:
- 大群聊用 Pull 模型(用户主动拉取,不推送)
- 消息分页加载(每次 50 条)
- 已读状态只统计”最近 N 条消息”
容量估算
| 指标 | 计算 | 结果 |
|---|---|---|
| WebSocket 连接 | 5 亿 DAU × 1.5 设备 | 7.5 亿并发连接 |
| 消息 QPS | 500 亿/天 | 58 万条/秒 |
| 存储 | 500 亿 × 500 字节 × 30 天 | 750TB |
| Kafka | 58 万 QPS | 200 个 partition |
实战问答 面试官常问 Trade-offs 与实战问答
问 Q1:你选择 WebSocket 还是长轮询?
候选人回答:
“我选择 WebSocket。它支持双向实时通信,延迟低(< 100ms),连接数少(一个连接处理双向消息)。长轮询需要频繁建立/断开连接,延迟高(> 1 秒),资源消耗大。WebSocket 是现代聊天系统的首选。”
面试官追问:
“如果客户端不支持 WebSocket 怎么办?”
候选人回答:
“降级到长轮询或 Server-Sent Events (SSE)。SSE 支持服务器推送,但不支持客户端发送消息。长轮询兼容性最好,但性能最差。我会优先检测客户端能力,自动选择最佳方案。“
问 Q2:如何保证消息不丢失?
候选人回答:
“At-least-once 投递保障。发送方发送消息后等待 ACK,超时则重试。接收方收到消息后发送 ACK。如果接收方未 ACK,消息存储在 Message Store,接收方上线后拉取。同时使用 Kafka 持久化 消息,防止服务器故障导致消息丢失。”
面试官追问:
“如果消息重复投递怎么办?”
候选人回答:
“幂等处理。每条消息有唯一 message_id,接收方检查是否已处理过该 message_id。如果已处理,直接丢弃。数据库使用唯一索引防止重复插入。“
问 Q3:消息顺序一致性怎么保证?
候选人回答:
“同一会话内的消息按时间排序。每条消息带 timestamp 和 sequence_number。客户端收到消息后,按 sequence_number 排序显示。如果网络延迟导致乱序,客户端重新排序。服务端使用 Kafka 分区 保证同一会话的消息顺序。”
面试官追问:
“如果用户同时在手机和电脑上登录,消息怎么同步?”
候选人回答:
“多设备独立连接,但共享同一个会话状态。消息投递到所有在线设备。已读状态同步:任一设备标记已读,所有设备更新。使用 Redis 同步会话状态,确保一致性。“
问 Q4:离线消息怎么处理?
候选人回答:
“用户离线期间,消息存储到 Message Store。用户上线时,拉取离线消息(从 last_read_msg_id 开始)。离线 < 7 天保留所有消息,离线 > 7 天只保留最近 1000 条。已读的离线消息可以删除,节省存储。”
面试官追问:
“如果用户离线太久,消息太多怎么办?”
候选人回答:
“限制离线消息数量(最近 1000 条)。同时提供’未读消息摘要’,告知用户有多少未读消息。对于重要消息(如 @提醒),可以推送通知。“
问 Q5:大群聊(1000+ 人)怎么设计?
候选人回答:
“大群聊用 Pull 模型。用户主动拉取消息,而不是推送。消息分页加载(每次 50 条)。已读状态只统计’最近 N 条消息’。同时限制大群聊的消息频率(如每秒 10 条),防止消息风暴。”
面试官追问:
“如果群聊消息太多,用户看不过来怎么办?”
候选人回答:
“提供消息摘要(如’10 条新消息’)。支持消息过滤(如只看 @自己的消息)。提供搜索功能,方便用户查找历史消息。“
进阶扩展方向
- 端到端加密: Signal Protocol、Double Ratchet
- 媒体消息: 先发送缩略图/占位符,再异步上传原文件
- 消息搜索: Elasticsearch 索引消息内容
- 跨平台同步: 多设备消息同步(手机、电脑、平板)
注意 常见踩坑点
| # | 踩坑点 | 解决方案 |
|---|---|---|
| 1 | 消息丢失:没有 ACK 机制或重试逻辑 | ACK + 重试 + 持久化 |
| 2 | 消息乱序:网络延迟导致消息到达顺序与发送顺序不一致 | sequence_number + 客户端重排序 |
| 3 | 重复消息:重试导致同一条消息被发送多次 | 幂等处理 + 唯一索引 |
| 4 | 僵尸连接:客户端断开但服务端不知道 | 心跳检测 + 超时断开 |
| 5 | 大群聊风暴:1000 人群聊每条消息广播 1000 次 | Pull 模型 + 分页 |
总结
实时聊天系统设计考察:
| 能力 | 考察点 |
|---|---|
| WebSocket 连接管理 | 长连接、心跳、粘滞会话 |
| 消息投递保障 | at-least-once、ACK、重试、幂等 |
| 离线消息 | 存储策略、上线同步、过期清理 |
| 一致性 | 消息顺序、状态同步、多设备协同 |
面试提示: 先画出消息从发送到接收的完整流程,然后逐步讨论每个环节的 edge case。面试官通常会追问”如果消息丢了怎么办”、“如何保证消息顺序”——准备好具体的技术方案。
推荐阅读
- 系统设计面试完全指南 — 掌握万能回答框架
- 设计 Twitter/微博信息流系统 — 高并发读写系统的设计思路
💡 需要面试辅导?
如果你对准备技术面试感到迷茫,或者想要个性化的面试指导和简历优化,欢迎联系 Interview Coach Pro 获取一对一辅导服务。
👉 联系我们 获取专属面试准备方案