即时通讯系统设计
问题
如何设计一个即时通讯(IM)系统?
答案
整体架构
核心流程
单聊消息投递
消息可靠投递
| 状态 | 含义 | 实现 |
|---|---|---|
| 已发送 | 服务端收到 | 服务端返回 ACK |
| 已送达 | 接收端收到 | 接收端返回 ACK |
| 已读 | 接收端已读 | 接收端发送已读回执 |
消息重传机制
public void sendMessage(Message msg) {
// 1. 存储消息,状态=发送中
messageMapper.insert(msg);
// 2. 推送给接收方
boolean pushed = pushService.push(msg.getToUserId(), msg);
if (!pushed) {
// 3. 推送失败(离线),存入离线队列
offlineMessageService.save(msg.getToUserId(), msg.getId());
}
// 4. 启动超时重试(5s 未收到 ACK 则重发)
retryScheduler.scheduleRetry(msg.getId(), 5, TimeUnit.SECONDS);
}
群聊消息
群聊扩散 —— 写扩散 vs 读扩散
// 写扩散:为每个群成员写一条消息记录
// 适合小群(<200人),查询快
public void sendGroupMessage(GroupMessage msg) {
List<Long> memberIds = groupService.getMemberIds(msg.getGroupId());
for (Long memberId : memberIds) {
// 写入每个成员的收件箱
inboxService.write(memberId, msg.getId());
}
}
// 读扩散:只存一条消息,读取时按群 ID 查
// 适合大群(>200人),写入快
消息存储设计
消息表(按时间分表)
CREATE TABLE message (
id BIGINT PRIMARY KEY,
conversation_id VARCHAR(64), -- 会话ID
from_uid BIGINT,
to_uid BIGINT,
content TEXT,
msg_type TINYINT, -- 文本/图片/语音/...
status TINYINT, -- 发送中/已送达/已读
created_at DATETIME,
INDEX idx_conversation (conversation_id, created_at)
);
已读回执
群聊已读回执
// 记录每个用户在每个会话中读到的最新消息 ID
// Redis Hash: read_cursor:{userId} → { conversationId: lastReadMsgId }
public void markAsRead(long userId, String conversationId, long msgId) {
redisTemplate.opsForHash().put(
"read_cursor:" + userId, conversationId, String.valueOf(msgId)
);
}
// 未读数 = 会话最新消息 ID - 用户已读消息 ID
public long getUnreadCount(long userId, String conversationId) {
long lastRead = getLastReadMsgId(userId, conversationId);
long latest = getLatestMsgId(conversationId);
return latest - lastRead;
}
常见面试问题
Q1: 如何保证消息不丢?
答案:
三重保障:
- 客户端 → 服务端:服务端收到后返回 ACK,超时重传
- 服务端 → MQ:MQ 事务消息或发送确认
- MQ → 接收端:接收端 ACK,未确认则重投
通过 msg_id 做幂等去重,防止重复消息。
Q2: 如何保证消息有序?
答案:
- 服务端用全局递增 ID(雪花算法)作为
msg_id - 客户端按
msg_id排序展示 - 同一会话的消息路由到同一 MQ 分区保证顺序消费
Q3: 大量用户同时在线,长连接如何管理?
答案:
- 连接层独立部署,无状态,水平扩展
- 用 Redis 维护
userId → 连接所在机器的路由表 - 跨机器推送通过 MQ/Redis Pub-Sub 转发
Q4: 离线消息怎么处理?
答案:
离线消息存储到离线表或 Redis,用户上线后拉取并清空。大量离线消息分页拉取,避免一次推送太多。