作为一名拥有八年 Java 后端开发经验的技术人员,我参与过多个大型 IM 系统的设计与实现。在这篇博客中,我将分享如何设计一个支持千万级用户的 IM 系统,并重点探讨消息推送可靠性的关键技术和实现方案。
业务场景分析
在设计 IM 系统之前,我们需要明确业务场景和需求:
- 用户规模:支持千万级在线用户,高峰期并发消息量可能达到每秒数万条
- 消息类型:文本、图片、语音、视频等多种消息格式
- 可靠性要求:消息不能丢失,重要消息需要确保送达
- 实时性要求:消息推送延迟控制在 1 秒以内
- 离线消息:用户离线时保存消息,上线后推送
- 多端同步:支持手机、平板、PC 等多端消息同步
架构设计概览
一个支持千万级用户的 IM 系统通常采用以下架构:
- 接入层:负载均衡、长连接管理、协议解析
- 服务层:消息处理、会话管理、好友关系、群组管理
- 存储层:消息存储、用户信息存储、离线消息存储
- 推送层:消息推送、离线推送、状态同步
- 监控层:系统监控、性能监控、故障预警
可靠性保障的核心技术方案
1. 消息确认机制
为确保消息可靠送达,我们需要实现严格的消息确认机制:
scss
/**
* 消息确认服务实现
* 负责处理消息的发送、确认和重试逻辑
*/
@Service
public class MessageAckServiceImpl implements MessageAckService {
// 消息确认超时时间(毫秒)
private static final long ACK_TIMEOUT = 30000;
// 最大重试次数
private static final int MAX_RETRIES = 5;
// 待确认消息缓存(消息ID -> 消息对象)
private final ConcurrentHashMap<String, Message> pendingMessages = new ConcurrentHashMap<>();
// 定时任务调度器
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
@Autowired
private MessageStore messageStore;
@Autowired
private PushService pushService;
@Override
public void sendMessage(Message message) {
// 1. 持久化消息
messageStore.saveMessage(message);
// 2. 发送消息
boolean success = pushService.pushToDevice(message.getReceiverId(), message);
if (success) {
// 3. 加入待确认队列
pendingMessages.put(message.getMessageId(), message);
// 4. 设置超时检查任务
scheduler.schedule(() -> checkMessageAck(message.getMessageId()),
ACK_TIMEOUT,
TimeUnit.MILLISECONDS);
} else {
// 发送失败,标记为失败状态
message.setStatus(MessageStatus.FAILED);
messageStore.updateMessageStatus(message.getMessageId(), MessageStatus.FAILED);
}
}
@Override
public void handleMessageAck(String messageId, String receiverId) {
// 移除待确认队列中的消息
Message message = pendingMessages.remove(messageId);
if (message != null) {
// 更新消息状态为已确认
message.setStatus(MessageStatus.CONFIRMED);
messageStore.updateMessageStatus(messageId, MessageStatus.CONFIRMED);
log.info("消息 {} 已确认接收", messageId);
}
}
private void checkMessageAck(String messageId) {
Message message = pendingMessages.get(messageId);
if (message != null) {
// 消息未确认,进行重试
message.incrementRetryCount();
if (message.getRetryCount() <= MAX_RETRIES) {
// 重试发送
boolean success = pushService.pushToDevice(message.getReceiverId(), message);
if (success) {
// 重新设置超时检查
scheduler.schedule(() -> checkMessageAck(messageId),
ACK_TIMEOUT,
TimeUnit.MILLISECONDS);
} else {
// 重试失败,标记为失败状态
message.setStatus(MessageStatus.FAILED);
messageStore.updateMessageStatus(messageId, MessageStatus.FAILED);
pendingMessages.remove(messageId);
log.error("消息 {} 重试 {} 次后仍然失败", messageId, message.getRetryCount());
}
} else {
// 超过最大重试次数,标记为失败
message.setStatus(MessageStatus.FAILED);
messageStore.updateMessageStatus(messageId, MessageStatus.FAILED);
pendingMessages.remove(messageId);
log.error("消息 {} 达到最大重试次数 {}", messageId, MAX_RETRIES);
// 通知发送方消息发送失败
notifySenderMessageFailed(message);
}
}
}
private void notifySenderMessageFailed(Message message) {
// 构建通知消息
Message notification = new Message(
UUID.randomUUID().toString(),
"system",
message.getSenderId(),
"消息发送失败:" + message.getMessageId()
);
// 发送通知消息
sendMessage(notification);
}
}
2. 多机房部署与故障转移
为保证系统高可用,我们采用多机房部署方案:
scss
/**
* 分布式消息路由服务
* 负责将消息路由到正确的机房和服务器
*/
@Service
public class MessageRouterServiceImpl implements MessageRouterService {
// 机房配置
private final Map<String, DataCenterConfig> dataCenters = new ConcurrentHashMap<>();
// 本地机房标识
private final String localDataCenterId;
// 用户会话路由表(用户ID -> 服务器ID)
private final LoadingCache<String, String> userSessionCache;
@Autowired
public MessageRouterServiceImpl(ConfigService configService) {
// 初始化机房配置
this.dataCenters.putAll(configService.loadDataCenterConfig());
this.localDataCenterId = configService.getLocalDataCenterId();
// 初始化用户会话缓存
this.userSessionCache = Caffeine.newBuilder()
.maximumSize(10_000_000) // 1000万用户缓存
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(this::fetchUserSession);
}
@Override
public String routeMessage(Message message) {
String receiverId = message.getReceiverId();
try {
// 从缓存获取用户会话所在服务器
String serverId = userSessionCache.get(receiverId);
if (isServerInLocalDataCenter(serverId)) {
// 本地机房服务器,直接返回
return serverId;
} else {
// 跨机房服务器,返回目标机房网关
String targetDataCenterId = getServerDataCenter(serverId);
return dataCenters.get(targetDataCenterId).getGatewayServerId();
}
} catch (ExecutionException e) {
log.error("获取用户会话失败: {}", receiverId, e);
// 降级处理:返回本地负载最低的服务器
return getLeastLoadedServer();
}
}
@Override
public void handleDataCenterFailure(String failedDataCenterId) {
// 1. 标记故障机房
DataCenterConfig failedConfig = dataCenters.get(failedDataCenterId);
if (failedConfig != null) {
failedConfig.setAvailable(false);
log.warn("机房 {} 已标记为不可用", failedDataCenterId);
}
// 2. 迁移故障机房的用户会话
migrateUserSessions(failedDataCenterId);
// 3. 通知其他机房故障信息
notifyOtherDataCenters(failedDataCenterId);
}
private void migrateUserSessions(String failedDataCenterId) {
// 获取故障机房的所有用户
List<String> affectedUsers = userSessionCache.asMap().entrySet().stream()
.filter(entry -> getServerDataCenter(entry.getValue()).equals(failedDataCenterId))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
log.info("需要迁移的用户数量: {}", affectedUsers.size());
// 逐个迁移用户会话
for (String userId : affectedUsers) {
try {
// 选择一个新的服务器
String newServerId = selectNewServerForUser(userId);
// 更新会话路由表
updateUserSession(userId, newServerId);
// 通知客户端重新连接
sendReconnectNotification(userId, newServerId);
} catch (Exception e) {
log.error("迁移用户会话失败: {}", userId, e);
}
}
}
// 其他方法实现...
}
3. 离线消息处理
为确保用户不会错过重要消息,需要实现可靠的离线消息存储和推送机制:
scss
/**
* 离线消息服务实现
* 负责处理用户离线时的消息存储和推送
*/
@Service
public class OfflineMessageServiceImpl implements OfflineMessageService {
// 离线消息队列
private final BlockingQueue<Message> offlineMessageQueue = new LinkedBlockingQueue<>(100000);
// 离线消息批量处理大小
private static final int BATCH_SIZE = 100;
// 离线消息处理器线程池
private final ExecutorService offlineProcessors = Executors.newFixedThreadPool(10);
@Autowired
private MessageStore messageStore;
@Autowired
private PushService pushService;
@PostConstruct
public void init() {
// 启动离线消息处理线程
for (int i = 0; i < 10; i++) {
offlineProcessors.submit(this::processOfflineMessages);
}
}
@Override
public void storeOfflineMessage(Message message) {
try {
// 1. 将消息加入离线队列
offlineMessageQueue.put(message);
log.debug("消息 {} 已加入离线队列,接收者: {}", message.getMessageId(), message.getReceiverId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("存储离线消息被中断", e);
}
}
private void processOfflineMessages() {
List<Message> batchMessages = new ArrayList<>(BATCH_SIZE);
while (true) {
try {
// 从队列获取消息,超时1秒防止线程阻塞
Message message = offlineMessageQueue.poll(1, TimeUnit.SECONDS);
if (message != null) {
batchMessages.add(message);
// 批量处理或等待队列满
if (batchMessages.size() >= BATCH_SIZE) {
processBatchMessages(batchMessages);
batchMessages.clear();
}
} else if (!batchMessages.isEmpty()) {
// 队列为空但有未处理消息,立即处理
processBatchMessages(batchMessages);
batchMessages.clear();
}
} catch (Exception e) {
log.error("处理离线消息出错", e);
// 发生异常时保存已处理的消息
if (!batchMessages.isEmpty()) {
saveProcessedMessages(batchMessages);
batchMessages.clear();
}
}
}
}
private void processBatchMessages(List<Message> messages) {
// 按接收者ID分组
Map<String, List<Message>> userMessages = messages.stream()
.collect(Collectors.groupingBy(Message::getReceiverId));
// 批量处理每个用户的离线消息
for (Map.Entry<String, List<Message>> entry : userMessages.entrySet()) {
String receiverId = entry.getKey();
List<Message> userMsgs = entry.getValue();
try {
// 检查用户是否在线
boolean isUserOnline = checkUserOnline(receiverId);
if (isUserOnline) {
// 用户已上线,尝试推送离线消息
for (Message msg : userMsgs) {
boolean success = pushService.pushToDevice(receiverId, msg);
if (success) {
// 更新消息状态为已投递
msg.setStatus(MessageStatus.DELIVERED);
messageStore.updateMessageStatus(msg.getMessageId(), MessageStatus.DELIVERED);
}
}
} else {
// 用户仍离线,持久化存储离线消息
storeMessagesForOfflineUser(receiverId, userMsgs);
}
} catch (Exception e) {
log.error("处理用户 {} 的离线消息失败", receiverId, e);
}
}
}
// 其他方法实现...
}
性能优化与扩展性设计
针对千万级用户的 IM 系统,我们还需要考虑以下性能优化和扩展性设计:
- 消息存储优化:采用分库分表策略,按用户 ID 哈希分片
- 缓存策略:高频访问数据(如用户在线状态)使用 Redis 缓存
- 异步处理:非核心业务(如消息计数、统计)采用消息队列异步处理
- 水平扩展:服务无状态设计,支持按需扩展服务器数量
- 熔断与限流:使用 Sentinel 或 Hystrix 防止服务雪崩
监控与告警
完善的监控系统是保证系统可靠性的重要组成部分:
- 连接监控:实时监控长连接数量、连接成功率、断开率
- 消息监控:监控消息发送成功率、延迟、堆积情况
- 性能监控:监控服务器 CPU、内存、网络带宽使用情况
- 告警机制:设置阈值自动触发告警,如消息堆积超过 10 万条
总结
设计一个支持千万级用户的 IM 系统并保证消息推送的可靠性是一个复杂的工程问题。通过采用消息确认机制、多机房部署、离线消息处理、以及完善的监控系统,我们可以构建一个高可用、高性能、高可靠的 IM 系统。
在实际开发过程中,还需要根据具体业务场景进行适当调整和优化。例如,对于金融类 IM 应用,可能需要更高的消息可靠性保证;而对于社交类应用,则可能更注重系统的扩展性和性能。