系统设计聊天系统WebSocket实时通信systemdesign面试

系统设计 Deep Dive:设计实时聊天系统

系统设计深度解析:设计实时聊天系统。涵盖WebSocket协议、消息队列、消息持久化与离线推送架构方案,还原真实系统设计面试的完整思考过程、技术选型权衡与面试官追问方向。

Sam · · 12 分钟阅读

面试官真实提问

“请设计一个实时聊天系统,类似 WhatsApp 或微信。支持单聊和群聊,消息需要保证送达,用户离线时消息需要保存。”

“如何处理消息的顺序一致性?如果用户同时在手机和电脑上登录,消息如何同步?”

这道题考察实时通信、消息队列、数据一致性和多设备同步的综合设计能力,在 Meta、Google、Amazon 的面试中频繁出现。


需求澄清清单

功能需求

Must-have:

  • 单聊(一对一实时消息)
  • 群聊(多人消息广播)
  • 消息持久化
  • 离线消息(用户上线后收到离线期间的消息)
  • 已读/未读状态

Nice-to-have:

  • 媒体消息(图片、视频、文件)
  • 消息撤回
  • 端到端加密
  • 消息搜索

规模估算

指标估算值
MAU10 亿
DAU5 亿
每日消息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 亿并发连接
消息 QPS500 亿/天58 万条/秒
存储500 亿 × 500 字节 × 30 天750TB
Kafka58 万 QPS200 个 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:消息顺序一致性怎么保证?

候选人回答:

“同一会话内的消息按时间排序。每条消息带 timestampsequence_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。面试官通常会追问”如果消息丢了怎么办”、“如何保证消息顺序”——准备好具体的技术方案。


推荐阅读


💡 需要面试辅导?

如果你对准备技术面试感到迷茫,或者想要个性化的面试指导和简历优化,欢迎联系 Interview Coach Pro 获取一对一辅导服务。

👉 联系我们 获取专属面试准备方案

准备好拿下下一次面试了吗?

获取针对你的目标岗位和公司的个性化辅导方案。

联系我们