高 困难
IM系统设计#
一句话答案#
IM 系统核心架构:客户端通过长连接(WebSocket/TCP)接入网关层,消息经 MQ 异步投递到接收方网关再推送,离线消息存 DB/Redis 拉取;通过消息 ID 去重 + ACK 确认 + 序号排序保证可靠有序。
核心要点
整体架构#
┌──────────┐ WebSocket ┌──────────────┐ MQ ┌──────────────┐ WebSocket ┌──────────┐
│ 发送方 A │ ──────────────→│ 接入网关层 │ ──────────→ │ 接入网关层 │──────────────→ │ 接收方 B │
└──────────┘ └──────┬───────┘ └──────────────┘ └──────────┘
│
┌─────┴─────┐
│ 消息服务 │
│ (存储+路由) │
└─────┬─────┘
│
┌───────────────┼───────────────┐
│ │ │
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ 消息 DB │ │ Redis │ │ MQ │
│(历史消息) │ │(在线状态) │ │(异步投递) │
└───────────┘ └───────────┘ └───────────┘plaintext核心设计点#
1. 连接管理
- 长连接协议:WebSocket(Web)/ 自定义 TCP(App)
- 心跳保活:客户端 30s 发一次心跳,服务端 90s 未收到断开
- 连接网关:维护 userId → channelId 映射,存 Redis(分布式场景)
2. 消息投递模型
在线投递(推模式):
A → 网关 → 消息服务(存DB + 写MQ)→ 路由查 B 在哪个网关 → 推送给 B
离线投递(拉模式):
B 上线 → 拉取离线消息(按 lastSeq 增量拉取)plaintext3. 消息可靠性
| 机制 | 作用 |
|---|---|
| 消息 ID(全局唯一) | 去重 |
| 序列号(递增 seq) | 排序 + 断点续拉 |
| 客户端 ACK | 确认收到,未 ACK 则重推 |
| 服务端持久化 | 消息入库后才返回发送成功 |
4. 消息存储
单聊:写扩散(每个人一份收件箱)
- 优点:读简单,按 userId 分库
- 缺点:写放大
群聊:读扩散(群消息存一份,成员各自记录已读位点)
- 优点:写不放大
- 缺点:读时需合并
- 大群(>500人)一般用读扩散 + 推送离线通知plaintext5. 群消息优化
- 小群(<200人):写扩散到每人收件箱
- 大群(>200人):读扩散 + 消息存群维度 + 维护每人 lastReadSeq
- 万人群:不推全量消息,只推通知(有新消息),客户端按需拉取
消息序号设计#
方案1:Redis INCR per conversation(简单,但 Redis 单点)
方案2:Snowflake 全局 ID(天然有序,但跨 conversation 不连续)
方案3:号段分配(发号器批量分配 seq 段,本地递增)
推荐:conversation 粒度的 Redis INCR + 本地缓存号段plaintext已读回执 & 消息撤回#
已读回执:B 读到 seq=100 → 发 ACK(conversation, seq=100) → A 端更新"已读"
消息撤回:2分钟内可撤回 → 发撤回消息 → 服务端标记 + 推送撤回通知给对方plaintext面试回答(2分钟版)
IM 系统核心分三层:接入层用 WebSocket 维护长连接,业务层处理消息路由和存储,存储层用 MySQL 存历史消息、Redis 存在线状态和未读计数。发消息流程:A 发消息到网关,消息服务先持久化再通过 MQ 异步路由到 B 所在网关推送。B 不在线则存离线消息,上线后按 lastSeq 增量拉取。可靠性靠三个机制保证:全局唯一消息 ID 去重、递增 seq 排序和断点续拉、客户端 ACK 确认未收到则重推。群消息的关键设计是写扩散和读扩散的选择:小群写扩散到每人收件箱方便读取,大群读扩散只存一份减少写放大。消息序号用 conversation 粒度的 Redis INCR。连接保活用 30s 心跳,超时断开触发离线流程。
追问与易错
追问方向:
- “怎么保证消息不丢?”→ 先持久化再 ACK 给发送方 + 接收方 ACK 确认
- “百万长连接怎么维护?”→ 多网关分摊 + epoll 单机 10w+ 连接 + 心跳检测
- “消息乱序怎么办?”→ 客户端按 seq 排序展示,发现 seq 断裂主动拉取
- “群消息已读怎么做?”→ 每人维护 lastReadSeq,对比群 maxSeq 算未读数
易错点:
- ❌ “所有消息都推模式”——离线消息必须拉模式,否则 MQ 积压
- ❌ “群聊用写扩散”——万人群写扩散会爆炸,大群必须读扩散
- ❌ “WebSocket 断了消息就丢了”——有离线存储 + 上线拉取兜底