跳到主要内容

即时通讯系统设计

问题

如何设计一个即时通讯(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: 如何保证消息不丢?

答案

三重保障:

  1. 客户端 → 服务端:服务端收到后返回 ACK,超时重传
  2. 服务端 → MQ:MQ 事务消息或发送确认
  3. MQ → 接收端:接收端 ACK,未确认则重投

通过 msg_id幂等去重,防止重复消息。

Q2: 如何保证消息有序?

答案

  • 服务端用全局递增 ID(雪花算法)作为 msg_id
  • 客户端按 msg_id 排序展示
  • 同一会话的消息路由到同一 MQ 分区保证顺序消费

Q3: 大量用户同时在线,长连接如何管理?

答案

  • 连接层独立部署,无状态,水平扩展
  • 用 Redis 维护 userId → 连接所在机器 的路由表
  • 跨机器推送通过 MQ/Redis Pub-Sub 转发

Q4: 离线消息怎么处理?

答案

离线消息存储到离线表或 Redis,用户上线后拉取并清空。大量离线消息分页拉取,避免一次推送太多。

相关链接