即时通讯(IM)是移动互联网最基础的实时能力:聊天消息要在几百毫秒内送达对端,单聊、群聊、频道消息要可靠存储,用户换了手机还能拉回全部历史记录,多端登录时 PC 与手机的消息进度要同步。本文按照系统设计面试的标准答题结构,设计一个生产级的即时通讯系统,覆盖长连接接入、消息存储、多端同步、群聊扩展与可靠性保证。
一句话:IM 的两条主线是「实时送达」与「可靠存储」——长连接负责把消息秒级推给在线用户,离线消息与多端同步靠「按序存储 + 游标拉取」保证不丢不乱。
一、需求澄清与量级估算
1.1 需求澄清
面试官给出题目「设计一个即时通讯系统」后,先通过提问明确边界:
- 聊天形态:单聊、群聊、频道/公众号、系统通知,覆盖哪些?
- 消息类型:文本、图片、语音、视频、位置、文件,是否都需要?
- 在线状态:是否需要显示在线/离线、正在输入?状态如何感知?
- 多端同步:手机、PC、平板多端同时在线,消息进度如何对齐?
- 历史消息:需要保存多久?支持搜索吗?
- 可靠性:弱网下消息如何保证不丢?是否需要已读回执?
- 规模:多少 DAU、多少峰值 QPS、平均在线连接数?
明确假设(面向面试的合理假设):
| 需求项 | 假设 |
|---|---|
| 聊天形态 | 单聊 + 群聊 + 频道 |
| 消息类型 | 文本、图片、语音、视频、文件 |
| 在线状态 | 在线/离线/离开,客户端上报 |
| 多端同步 | 多端在线,按 seq 游标对齐 |
| 历史消息 | 永久保存,支持按会话拉取 |
| 可靠性 | 不丢消息 + 已读回执 |
| 规模 | 1 亿 DAU、峰值消息 100 万条/秒 |
1.2 量级估算
| 指标 | 估算值 | 推导 |
|---|---|---|
| DAU | 1 亿 | 大型国民级 IM |
| 日消息量 | 500 亿条 | 人均日发 50 条 |
| 峰值消息 | 100 万条/秒 | 晚高峰 + 群发场景 |
| 在线连接 | 5000 万 | 约 50% 用户在线 |
| 单连接带宽 | ~5KB/s | 心跳 + 消息推送 |
| 消息存储 | 日均 PB 级 | 文本小、媒体大(另存 OSS) |
| 送达时延 | P99 < 500ms | 在线推送目标 |
| 群聊峰值 | 万人群广播 | 单群单条消息扇出 1 万 |
一句话:1 亿 DAU、100 万条/秒的消息,决定了 IM 必须是「长连接网关 + 消息存储 + 推送」的三段式架构——连接与存储解耦,网关无状态可水平扩。
二、高层架构设计
┌─────────────────────────┐ ┌─────────────────────────┐
│ 用户 A (手机/PC 多端) │ │ 用户 B (手机/PC 多端) │
└────────────┬────────────┘ └────────────┬────────────┘
│ WebSocket 长连接 │ WebSocket 长连接
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ 接入网关层 (Connection Gateway) │
│ ① 连接管理/心跳 ② 连接路由表(uid→conn) ③ 消息收发代理 │
│ 无状态,可水平扩展;用 Redis 维护全局在线路由 │
└────────────┬────────────────────────────────┬───────────┘
│ 上行消息(发消息) │ 下行推送(推消息)
▼ │
┌──────────────────────────────────────────────▼──────────┐
│ 消息服务层 (Msg Service) │
│ ① 消息路由与 seq 分配 ② 消息持久化 ③ 在线/离线分发 │
│ ④ 已读回执 ⑤ 群聊扇出 │
└────────────┬────────────────────────────────────────────┘
│
┌────────────▼────────────────────────────────────────────┐
│ 存储层 │
│ MySQL:会话/消息索引、用户关系 │
│ KV(Cassandra/TiKV):按 (session, seq) 的时序消息存储 │
│ Redis:在线状态、连接路由、未读数 │
└────────────┬────────────────────────────────────────────┘
│
┌────────────▼────────────────────────────────────────────┐
│ 离线与推送:用户离线 → 消息存离线库 + 走 APNs/厂商推送 │
│ 媒体:图片/语音/视频 存 OSS,消息体只存 URL │
└─────────────────────────────────────────────────────────┘
整体拆为四层:
- 接入网关层:维护海量 WebSocket 长连接,负责心跳、鉴权、连接路由。
- 消息服务层:消息路由、seq 分配、持久化、在线/离线分发、群聊扇出。
- 存储层:时序消息存储 + 会话索引 + 在线状态,KV 与 MySQL 分工。
- 离线与媒体:离线消息缓存 + 厂商推送 + 媒体对象存储。
2.1 长连接 vs 短轮询
实时送达的前提是「服务端能主动找到用户」,长连接是唯一可行解:
短轮询:客户端每秒问「有新消息吗」——1 亿 DAU 每秒 1 亿次查询,不可行
长轮询:HTTP 挂起 30 秒等消息——连接频繁重建,实时性差
WebSocket 长连接:一次握手后双向全双工——服务端可随时推送,最贴合 IM
选型:WebSocket(HTTP 兼容、穿透性好);弱网退化为「长轮询 + 厂商推送」兜底
一句话:IM 实时性的根基是长连接——连接建立后由服务端「主动推送」而非客户端「轮询拉取」,在线消息的送达时延因此压到毫秒级。
三、核心组件设计
3.1 连接管理与会话
网关维护每条连接的会话,核心是「连接路由表」与「心跳保活」:
连接生命周期:
① 客户端建连 → 鉴权(携带 token) → 绑定 uid
② 建立连接路由:redis hset conn:{uid} gateway_id, conn_id
③ 心跳:客户端每 30s 发 ping,网关 60s 未收到则判定离线
④ 断线重连:客户端自动重连,带上 last_seq 续拉未同步消息
路由表:
conn:{uid} → {gateway_id, conn_id, platform, online}
推送时先查路由表定位 uid 所在网关 → 网关找到连接 → 下发
多端策略:
同 uid 多端(手机+PC)都建立连接,路由表存多行
下行消息「推全端」,各自按 seq 对齐
3.2 消息模型与存储
每条消息需要唯一、有序、全局可定位的编号——msgid 与 会话内 seq:
CREATE TABLE message (
msg_id BIGINT PRIMARY KEY, -- 全局唯一,服务端分配
session_id BIGINT, -- 会话(单聊=两人 hash,群聊=群 id)
sender_id BIGINT,
msg_type TINYINT, -- 1文本 2图片 3语音 4视频 5文件
content TEXT, -- 文本内容 / 媒体 URL
seq BIGINT, -- 会话内递增序号
send_time DATETIME,
status TINYINT -- 0正常 1撤回 2被删除
);
-- 会话索引:记录会话成员与最后一条消息,用于会话列表
CREATE TABLE conversation (
user_id BIGINT,
peer_id BIGINT, -- 对方/群 id
last_msg_id BIGINT,
unread_count INT,
PRIMARY KEY (user_id, peer_id)
);
seq 分配:
单聊会话:seq 在会话内严格递增,由消息服务统一分配
群聊会话:群内每成员看到的 seq 一致(群消息共享序号)
客户端拉取:SELECT * FROM message WHERE session_id=? AND seq > ? ORDER BY seq
要点:
seq是会话内单调递增的「水位线」,客户端记录自己已同步到的max_seq,重连后从max_seq+1续拉——这就是多端同步与断点续传的数学基础。
3.3 多端同步:seq 游标
多端同步的核心是「每端各自记录拉取进度,不依赖对端状态」:
每个 (uid, device) 维护一个游标 checkpoint:
cp:{uid}:{device} → last_seq(该端已同步的会话 seq)
同步流程:
① 上线/重连 → 读本地 cp
② 拉取 cp+1 之后的增量消息
③ 拉取完成 → 推进 cp
④ 新消息推送到达时顺带把对端 cp 更新(增量推送)
消息顺序保证:
同一会话的推送与拉取都按 seq 有序
客户端以 seq 为排序键,天然有序
一句话:IM 的「多端同步」不是一个复杂的同步协议,而是「每端一个单调游标 + 按 seq 增量拉取」——谁落后谁拉,游标永不回退,顺序永不乱。
3.4 群聊:读扩散 vs 写扩散
群聊是 IM 最大的扩展性挑战,关键在「一条消息如何到达群里所有人」:
写扩散(发消息时给每个成员都存一条):
优点:读时只查自己的收件箱,简单
缺点:万人群一条消息写 1 万条存储,写放大严重
读扩散(群里只存一条,成员按群 seq 拉取):
优点:写一次,存储省
缺点:读时要合并多群 + 查成员,读放大;大群拉取慢
折中(生产方案):
小群(<500 人)→ 写扩散:体验好,存储可接受
大群/频道 → 读扩散 + 在线推送扇出:离线只存「群游标」
;; 伪代码:大群读扩散发送
(defn send-group-msg [group-id sender content]
(let [seq (next-seq group-id)]
(msg-store/append! {:session_id group-id :seq seq
:sender_id sender :content content})
;; 在线成员直接推网关(连接路由批量查)
(doseq [uid (online-members group-id)]
(push! uid {:group_id group-id :seq seq :content content}))
;; 离线成员不逐条存,只推进群游标
(mark-offline-cursor! group-id seq)))
结论:群聊没有银弹——小群写扩散保证拉取快,大群读扩散保证不写爆存储;「在线走推送、离线只记游标」是万人群既不丢消息又不放大写量的关键。
3.5 可靠性与已读回执
弱网下消息可能丢,必须有「确认-重传」机制:
上行可靠性(客户端 → 服务端):
客户端发送时带 client_msg_id(客户端生成的唯一 id)
服务端幂等:同 client_msg_id 只落一条,返回服务端 msg_id
客户端收到 msg_id 才算发送成功,否则重发
下行可靠性(服务端 → 客户端):
服务端推送后等客户端 ack(ack 携带已接收的 seq)
未 ack 的消息 → 进入「待重推队列」,重连后按 seq 重推
推送与拉取共用同一 seq 水位,天然对账
已读回执:
客户端上报「已读到 seq=n」
服务端更新会话已读水位 → 通知对方
群聊已读只回执到「已读人数」级别,避免全量明细
;; 伪代码:上行幂等接收
(defn receive-msg [client-msg-id sender content]
(if-let [existing (redis/get (str "cm:" client-msg-id))]
{:msg_id existing} ; 重复消息,返回旧 id
(let [msg-id (alloc-msg-id!)]
(redis/set (str "cm:" client-msg-id) msg-id "NX" "EX" 86400)
(msg-store/append! {:msg_id msg-id :sender_id sender :content content})
{:msg_id msg-id})))
;; 伪代码:下行确认-重推
(defn on-ack [uid device seq]
(advance-ack-cursor! uid device seq)
(remove-pending uid device seq)) ; 从待重推队列移除
一句话:IM 的可靠性是「幂等 + ack」:上行用
client_msg_id幂等去重,下行用seq ack + 重推保证到达;只要 seq 不丢,消息就一条都不会丢。
四、深入权衡
4.1 在线状态如何感知
| 方案 | 实时性 | 代价 | 适用 |
|---|---|---|---|
| 心跳超时判定 | 秒级延迟 | 心跳有带宽成本 | 通用 |
| 客户端显式上报 | 即时 | 依赖客户端配合 | 状态切换场景 |
| 在线路由 + 心跳 | 秒级 + 精确定位 | Redis 压力可控 | 生产标配 |
结论:在线状态不追求「绝对实时」,心跳超时判定(30s 内未心跳即离线)即可满足;精确到「在哪个网关」的连接路由由 Redis 维护,供推送寻址。
4.2 消息存储选型
| 数据 | 存储 | 理由 |
|---|---|---|
| 消息时序数据 | 分布式 KV(Cassandra/TiKV) | 按 (session,seq) 顺序写、范围读 |
| 会话/关系/索引 | MySQL | 事务、低频 CRUD |
| 在线状态/路由 | Redis | 高频读写、需快速失效 |
| 媒体文件 | OSS 对象存储 | 大对象、低成本、CDN 分发 |
结论:消息是「写多读多的时序数据」,天然适合 KV 的
(partition_key, seq)模型;MySQL 不适合做百万 TPS 的追加写,但适合做会话与关系的元数据存储。
4.3 群聊扇出的写放大
万人群若写扩散,一条消息 = 1 万次存储写,峰值 100 万条/秒会放大成 100 亿次/秒。权衡:
纯读扩散:写放大 = 1,读合并成本高
纯写扩散:读放大 = 1,写放大 = 群规模
工程方案:
在线成员(约 60%)→ 网关推送,不落存储
离线成员 → 只推进群游标(1 次写)
实际写放大 ≈ 在线成员数,但推送走内存连接而非存储,代价可控
一句话:群消息的成本大头不在「存储」而在「扇出」——把扇出走内存推送、把落库压到每次一条,万人群才可能撑住;这正是 IM 网关层存在的意义。
五、总结
即时通讯系统的骨架是「长连接接入 + 可靠存储 + 游标同步」三件套:接入网关层用 WebSocket 维护海量长连接并维护连接路由,消息服务层为每条消息分配会话内单调 seq 并持久化到 KV 存储,多端通过「每端一个游标 + 按 seq 增量拉取」实现同步。可靠性上,上行用 client_msg_id 幂等去重、下行用 ack 确认重推,配合心跳与断线重连,保证「消息一条不丢、顺序一条不乱」。群聊与频道采用「小群写扩散 + 大群读扩散」折中,在线走推送、离线记游标,在体验与写放大之间取得平衡。最终,IM 对用户表现为「秒达、不丢、不乱、多端对齐」的可靠体验。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。