一句话总结
IM系统核心:WebSocket长连接(全双工通信)+ 消息可靠性(ACK+重试+seqId去重)+ 消息存储(单聊写扩散、群聊读扩散)。单聊:写扩散(一条消息写两份,各自拉取,简单可靠)。群聊:读扩散(一条消息写一份,群成员拉取,节省存储)。消息同步:seqId 递增序列号,客户端记录已收到的最大 seqId,增量拉取。
初级理解
IM 系统架构
# IM 系统分层
# 1. 接入层(Netty + WebSocket):管理长连接
# 2. 逻辑层:消息路由、存储、推送
# 3. 存储层:MySQL(消息)+ Redis(路由表+缓存)
# 4. 推送层:在线推送(WebSocket)+ 离线推送(APNs/FCM)
# 核心流程
# 用户A发消息给用户B
# 1. A → 接入服务器:发送消息
# 2. 接入服务器 → 逻辑服务器:路由消息
# 3. 逻辑服务器:存储消息 + 查路由表(B在哪台服务器)
# 4. 逻辑服务器 → B的接入服务器:转发消息
# 5. B的接入服务器 → B:推送消息
消息可靠性四要素
# 1. ACK 确认
# 客户端收到消息 → 回复 ACK(包含 msgId)
# 服务端收到 ACK → 标记已送达
# 2. 重试机制
# 服务端发送消息 → 等待 ACK(超时 5s)
# 未收到 ACK → 重试(最多 3 次)
# 3 次失败 → 标记失败,走离线推送
# 3. 消息去重
# 每条消息有唯一 msgId
# 客户端维护已收到 msgId 集合
# 重复消息直接丢弃
# 4. seqId 顺序号
# 每个用户的消息有递增 seqId
# 客户端记录 lastSeqId
# 断线重连后拉取 seqId > lastSeqId 的消息
一句话总结:ACK+重试保证送达,msgId去重,seqId保证顺序和增量同步。
中级深入
写扩散 vs 读扩散
# 写扩散(单聊推荐)
# 一条消息写两份(发送方+接收方各自的消息表)
# 用户拉取自己的消息表即可
# 优点:读取简单,各自独立
# 缺点:写入放大(群聊时严重)
# 读扩散(群聊推荐)
# 一条消息只写一份(群消息表)
# 群成员拉取群消息表
# 优点:写入简单,节省存储
# 缺点:读取时需要查群消息表
# 实际方案:混合
# 单聊(1对1):写扩散
# 群聊(1对多):读扩散
# 超大群(万人大群):写扩散 + 异步推送
消息存储设计
# 单聊消息表(写扩散)
CREATE TABLE msg_single (
id BIGINT PRIMARY KEY,
seq_id BIGINT NOT NULL, # 用户维度递增
from_uid BIGINT,
to_uid BIGINT,
content TEXT,
msg_type TINYINT, # 1文本 2图片 3语音
created_at DATETIME,
INDEX idx_uid_seq (to_uid, seq_id) # 拉取消息
);
# 群聊消息表(读扩散)
CREATE TABLE msg_group (
id BIGINT PRIMARY KEY,
group_id BIGINT,
from_uid BIGINT,
content TEXT,
msg_type TINYINT,
created_at DATETIME,
INDEX idx_group_time (group_id, created_at)
);
# 用户消息同步位点
CREATE TABLE msg_sync (
user_id BIGINT PRIMARY KEY,
last_seq_id BIGINT, # 单聊最大 seqId
last_group_msg_id BIGINT # 群聊最大 msgId
);
中级要点:单聊写扩散(一人一份),群聊读扩散(一份共享),混合使用。
高级拓展
群聊消息推送优化
# 万人大群推送挑战
# 问题:一条消息要推送给 10000 人
# 写扩散:10000 次写入,不可接受
# 读扩散:10000 人拉取,DB 压力大
# 优化方案
# 1. 读扩散 + 缓存
# 群消息写一份 → Redis 缓存最新消息
# 群成员从 Redis 拉取(减少 DB 压力)
# 2. 在线批量推送
# 群成员中在线的直接推送
# 离线的走离线消息
# 3. 分页拉取
# 用户进入群聊 → 拉取最近 50 条
# 上滑 → 拉取更早的消息
# 4. 消息压缩
# 多条消息合并推送(如 100ms 内的消息)
已读/未读设计
# 单聊已读回执
# 1. 用户B查看消息 → 发送已读回执(包含最后一条消息的 seqId)
# 2. 服务端更新已读位点
# 3. 推送已读回执给用户A
# 群聊已读(显示多少人已读)
# 1. 每条群消息记录已读人数
# 2. 用户查看消息 → 更新已读位点
# 3. 异步统计已读人数(不实时,延迟可接受)
# 未读数计算
# 总消息数 - 已读消息数 = 未读数
# Redis 存储未读数(实时更新)
# 或客户端本地计算
实战场景
场景:单聊消息发送
@Service
public class MessageService {
public void sendMessage(Long fromUid, Long toUid, String content) {
# 1. 生成消息
Message msg = new Message();
msg.setMsgId(snowflake.nextId());
msg.setFromUid(fromUid);
msg.setToUid(toUid);
msg.setContent(content);
# 2. 写扩散:存两份
long seqA = redis.incr("seq:" + fromUid);
long seqB = redis.incr("seq:" + toUid);
msgMapper.insert(msg, fromUid, seqA); # 发送方
msgMapper.insert(msg, toUid, seqB); # 接收方
# 3. 查路由表
String serverIp = redis.get("route:" + toUid);
# 4. 在线 → 推送;离线 → 存离线消息
if (serverIp != null) {
pushService.push(serverIp, msg);
} else {
offlineService.save(toUid, msg);
}
}
}
面试模拟
面试官:IM系统怎么保证消息不丢不重?
你:四层保障:1)ACK确认:客户端收到后回复ACK,服务端超时重试(最多3次);2)消息去重:每条消息有唯一msgId,客户端去重;3)seqId顺序号:保证消息顺序,断线重连后增量拉取;4)离线消息:用户不在线时存DB,上线后拉取。
面试官:写扩散和读扩散怎么选?
你:单聊用写扩散(一条消息写两份),读取简单各自拉取;群聊用读扩散(一条消息写一份),节省存储。万人大群用读扩散+缓存优化。实际微信是写扩散(每人一份消息表),钉钉是读扩散(群消息共享)。