聊天系统设计面试题:设计 WhatsApp / 微信实时对话系统
聊天系统设计和对话系统面试题解析,覆盖 WebSocket、消息队列、消息顺序、离线消息、多设备同步和大群聊扩展,适合系统设计面试准备。
快速答案:聊天系统 / 对话系统设计面试怎么答
如果面试题是“设计 WhatsApp、微信、Slack 或实时聊天系统”,核心不是只说 WebSocket,而是要讲清楚一条消息从发送、持久化、路由、投递、ACK、重试到多设备同步的完整链路。高分答案通常围绕 WebSocket Gateway + Chat Service + Message Queue + Message Store + Presence Service 展开,再补充消息顺序、离线消息和大群聊的 trade-off。
这类 chat system design interview 最容易丢分的点,是没有区分单聊和群聊,没有解释 at-least-once 投递与幂等去重,也没有说明同一会话内如何保证消息顺序。
| 搜索/面试意图 | 回答重点 |
|---|---|
| 聊天系统设计面试题 | 先画发送方到接收方的消息链路,再拆 WebSocket、Kafka、存储和 ACK |
| 对话系统 / 实时通信系统 | 重点讲长连接、presence、心跳、断线重连、离线消息拉取 |
| WebSocket 系统设计 | 说明连接路由表、sticky session、gateway 扩展和降级方案 |
| 消息顺序一致性 | 用 session-level sequence number、Kafka partition 和客户端重排处理乱序 |
面试官真实提问
“请设计一个实时聊天系统,类似 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 索引消息内容
- 跨平台同步: 多设备消息同步(手机、电脑、平板)
面试官评分点:这道题怎么被打分
| 评分维度 | 高分表现 | 低分表现 |
|---|---|---|
| 链路完整性 | 主动画出 发送→Gateway→Kafka→Store→ACK 全链路,再逐个环节讨论 | 只说”用 WebSocket 推消息”就停了 |
| 投递保障 | 明确 at-least-once + 幂等去重 + ACK 超时重试,并解释为什么不选 exactly-once | 声称”消息绝不重复”但说不出去重机制 |
| 顺序与一致性 | session 级 sequence number + 同会话同 partition + 客户端重排 | 说”按时间戳排序”(时钟不可靠) |
| 规模感 | 给出 DAU、QPS、存储估算,并指出 7.5 亿长连接对 Gateway 的压力 | 全程没有数字 |
| trade-off 意识 | 主动讨论 push vs pull、大群聊 fanout 成本、离线消息保留策略 | 每个方案只给”最优解”,没有代价分析 |
一句话提醒: 这道题的及格线是链路画完整,高分线是主动说出自己方案的弱点(比如 sticky session 的扩展性、Kafka 的延迟开销)——面试官在测的是工程判断,不是背诵。
45 分钟答题模板
| 时间 | 做什么 | 产出 |
|---|---|---|
| 0-5 min | 需求澄清:单聊/群聊规模、延迟目标、在线率、设备数 | 3 个功能需求 + 2 个非功能需求 |
| 5-8 min | 容量估算:DAU → 并发连接数 → 消息 QPS → 日存储量 | 一张估算表 |
| 8-18 min | 高层架构:Gateway + Chat Service + Kafka + Message Store + Presence | 一张架构图 |
| 18-30 min | 深入 2 个核心:① 消息投递与 ACK/重试/幂等 ② 顺序一致性(sequence + partition) | 两个能扛住追问的设计 |
| 30-40 min | 扩展性:Gateway 水平扩展、大群聊 push→pull、离线消息策略 | 1 个 scale-up 方案 + trade-off |
| 40-45 min | 收尾:监控指标(投递延迟、ACK 超时率、在线连接数)+ 主动承认 1 个弱点 | 面试印象分 |
时间分配的关键:前 15 分钟必须画出完整架构——很多候选人在需求澄清上花了 20 分钟,后面只能讲 API。
和其他系统设计题的区别
| 维度 | 聊天系统(本文) | 信息流系统 | 推荐系统 |
|---|---|---|---|
| 核心矛盾 | 实时投递 + 顺序一致性 | 读多写多 + 热度/相关性排序 | 个性化 + 召回/排序/重排 pipeline |
| 连接模型 | 长连接(WebSocket)为主 | 短请求(REST/gRPC)为主 | 短请求 + 离线批处理 |
| 一致性要求 | 会话内顺序,at-least-once | 最终一致即可 | 不敏感 |
| 最热追问 | 消息丢了/乱序/重复怎么办 | fanout 推还是拉 | 冷启动、实时性 |
答题迁移提示: 如果面试官把题从聊天改到”设计 Slack 大群频道”,本质是群聊 fanout 问题——把时间花在 1000+ 人 broadcast 的 push/pull 选择上,而不是重画整个架构。
聊天系统设计 FAQ
聊天系统设计面试中 WebSocket 是必选吗?
大多数实时聊天系统会优先选择 WebSocket,因为它支持双向通信和低延迟推送。但面试里要主动补充降级方案:如果网络、代理或客户端限制导致 WebSocket 不稳定,可以降级到长轮询或 SSE;移动端还要考虑后台状态和 push notification。
对话系统如何保证消息顺序?
同一会话内通常用递增的 sequence_number 或逻辑时钟来排序,并让同一个 conversation 的消息进入同一个 Kafka partition。客户端收到乱序消息时可以按序号重排,缺失序号则触发补拉。
离线消息应该推送还是用户上线后拉取?
小规模或强实时消息可以推送通知,但完整消息列表通常在用户上线后按 last_read_msg_id 或 last_seen_sequence 拉取。这样可以控制移动端功耗,也能避免离线太久时一次性推送大量消息。
大群聊为什么不能简单广播?
如果一个 10 万人群每条消息都 fanout 到所有成员,写放大会非常高。面试里可以说小群用 push,大群用 pull 或 hybrid fanout,只给在线活跃用户推送,历史消息按游标分页拉取。
注意 常见踩坑点
| # | 踩坑点 | 解决方案 |
|---|---|---|
| 1 | 消息丢失:没有 ACK 机制或重试逻辑 | ACK + 重试 + 持久化 |
| 2 | 消息乱序:网络延迟导致消息到达顺序与发送顺序不一致 | sequence_number + 客户端重排序 |
| 3 | 重复消息:重试导致同一条消息被发送多次 | 幂等处理 + 唯一索引 |
| 4 | 僵尸连接:客户端断开但服务端不知道 | 心跳检测 + 超时断开 |
| 5 | 大群聊风暴:1000 人群聊每条消息广播 1000 次 | Pull 模型 + 分页 |
总结
实时聊天系统设计考察:
| 能力 | 考察点 |
|---|---|
| WebSocket 连接管理 | 长连接、心跳、粘滞会话 |
| 消息投递保障 | at-least-once、ACK、重试、幂等 |
| 离线消息 | 存储策略、上线同步、过期清理 |
| 一致性 | 消息顺序、状态同步、多设备协同 |
面试提示: 先画出消息从发送到接收的完整流程,然后逐步讨论每个环节的 edge case。面试官通常会追问”如果消息丢了怎么办”、“如何保证消息顺序”——准备好具体的技术方案。
推荐阅读
- 系统设计面试完全指南 — 掌握万能回答框架
- HDFS / 分布式文件系统 — 另一类高难度存储题:元数据、副本、容错的完整拆解
- 设计 Twitter/微博信息流系统 — 高并发读写系统的设计思路
- 设计推荐系统 — 同样需要处理在线服务、消息队列、缓存和扩展性权衡
💡 需要面试辅导?
如果你对准备技术面试感到迷茫,或者想要个性化的面试指导和简历优化,欢迎联系 Interview Coach Pro 获取一对一辅导服务。
- 👉 System Design 模拟面试:L4/L5/L6 评分 rubric、6 周训练路线与 mock 后 24 小时书面反馈
- 👉 联系我们 获取专属面试准备方案
相关面试辅导
如果你正在准备类似面试,可以直接从下面的专项辅导开始。