基于SpringBoot+RabbitMQ+MySQL+Redis实现高性能私信系统

6 阅读8分钟

基于 SpringBoot+RabbitMQ+Redis 的高性能私信系统:设计、实现与源码开发

在社交、协作类应用中,私信系统的稳定性、实时性直接决定用户体验。面对高并发、多端同步、消息可靠投递等核心诉求,传统同步架构难以支撑。本文基于 SpringBoot+RabbitMQ+Redis+MySQL 技术栈,从架构设计、核心功能实现到源码开发,完整拆解高性能私信系统的构建过程,助力开发者快速落地同类需求。

一、系统设计核心:架构与技术选型

1.1 整体架构分层

采用 “分层解耦 + 异步化” 架构,自上而下分为 5 层,确保职责单一、可扩展:

  • 接入层:API 网关(鉴权、限流、路由)+ WebSocket(实时推送)
  • 应用层:消息发送 / 接收、状态同步、历史消息查询等核心服务
  • 中间件层:RabbitMQ(异步削峰、可靠投递)+ Redis(缓存加速、状态同步)
  • 存储层:MySQL(消息持久化)+ MinIO(多媒体文件存储)
  • 监控层:Actuator + Prometheus(指标监控)

1.2 核心技术栈选型

组件选型核心作用
开发框架SpringBoot 2.7.x快速搭建微服务,简化配置
消息中间件RabbitMQ 3.12.x异步解耦、削峰填谷、可靠投递
缓存中间件Redis 6.x缓存未读消息、会话列表、已读状态
数据库MySQL 8.0消息最终持久化、用户关系存储
序列化Protobuf 3.x高效序列化,减少网络传输体积
实时推送Spring WebSocket服务端向客户端实时推送消息
连接池HikariCP数据库连接池优化,提升并发处理能力

二、核心功能实现与可复用代码

2.1 环境配置:基础依赖与配置文件

2.1.1 Maven 依赖(pom.xml)
<!-- SpringBoot核心依赖 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.15</version>
    <relativePath/>
</parent>

<<dependencies>
    <!-- Web核心 + WebSocket -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>

    <!-- RabbitMQ -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>

    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- MySQL + MyBatis-Plus -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>

    <!-- Protobuf -->
    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java</artifactId>
        <version>3.24.4</version>
    </dependency>

    <!-- 工具类 -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.22</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</</dependencies>
2.1.2 配置文件(application.yml)
spring:
  # 数据库配置
  datasource:
    url: jdbc:mysql://localhost:3306/im_message?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 300000

  # Redis配置
  redis:
    host: localhost
    port: 6379
    password:
    database: 0
    timeout: 2000ms
    lettuce:
      pool:
        max-active: 16
        max-idle: 8
        min-idle: 4

  # RabbitMQ配置
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    # 生产者确认
    publisher-confirm-type: correlated
    publisher-returns: true
    # 消费者配置
    listener:
      simple:
        acknowledge-mode: manual # 手动ACK
        concurrency: 5 # 核心线程数
        max-concurrency: 10 # 最大线程数
        prefetch: 10 # 每次拉取消息数

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.im.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 自定义配置
im:
  rabbitmq:
    exchange-name: im-message-exchange # 交换机名称
    single-queue-name: im-single-queue # 单聊队列
    group-queue-name: im-group-queue # 群聊队列
    dead-letter-exchange: im-dead-letter-exchange # 死信交换机
    dead-letter-queue: im-dead-letter-queue # 死信队列
  redis:
    unread-prefix: "im:unread:" # 未读消息前缀
    session-prefix: "im:session:" # 会话列表前缀
    read-prefix: "im:read:unsync:" # 未同步已读消息前缀

2.2 数据模型设计(Entity + Mapper)

2.2.1 消息实体(Message.java)
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("t_im_message")
public class Message {
    /**
     * 消息ID(雪花算法生成)
     */
    @TableId(type = IdType.INPUT)
    private Long id;

    /**
     * 发送者ID
     */
    private Long senderId;

    /**
     * 接收者ID(单聊:用户ID;群聊:群ID)
     */
    private Long receiverId;

    /**
     * 消息类型:1-文本;2-图片;3-语音
     */
    private Integer type;

    /**
     * 消息内容(文本直接存;多媒体存URL)
     */
    private String content;

    /**
     * 消息状态:0-待送达;1-已送达;2-已读
     */
    private Integer status;

    /**
     * 发送时间
     */
    private LocalDateTime sendTime;

    /**
     * 送达时间
     */
    private LocalDateTime deliverTime;

    /**
     * 已读时间
     */
    private LocalDateTime readTime;
}
2.2.2 Mapper 接口(MessageMapper.java)

java

运行

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.im.entity.Message;
import org.apache.ibatis.annotations.Param;
import java.util.List;

public interface MessageMapper extends BaseMapper<Message> {
    /**
     * 批量插入消息
     */
    int batchInsert(@Param("list") List<Message> messageList);

    /**
     * 批量更新消息状态为已读
     */
    int batchUpdateReadStatus(@Param("ids") List<Long> messageIds, @Param("readTime") LocalDateTime readTime);

    /**
     * 分页查询历史消息
     */
    List<Message> queryHistoryMessage(
            @Param("senderId") Long senderId,
            @Param("receiverId") Long receiverId,
            @Param("lastId") Long lastId,
            @Param("size") Integer size
    );
}

2.3 RabbitMQ 配置:交换机、队列与绑定

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@Slf4j
public class RabbitMQConfig {

    @Value("${im.rabbitmq.exchange-name}")
    private String exchangeName;

    @Value("${im.rabbitmq.single-queue-name}")
    private String singleQueueName;

    @Value("${im.rabbitmq.group-queue-name}")
    private String groupQueueName;

    @Value("${im.rabbitmq.dead-letter-exchange}")
    private String deadLetterExchange;

    @Value("${im.rabbitmq.dead-letter-queue}")
    private String deadLetterQueue;

    // 1. 声明死信交换机和队列
    @Bean
    public DirectExchange deadLetterExchange() {
        return ExchangeBuilder.directExchange(deadLetterExchange).durable(true).build();
    }

    @Bean
    public Queue deadLetterQueue() {
        return QueueBuilder.durable(deadLetterQueue).build();
    }

    @Bean
    public Binding deadLetterBinding() {
        return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with("dead-letter-key");
    }

    // 2. 声明单聊队列(绑定死信交换机)
    @Bean
    public Queue singleMessageQueue() {
        return QueueBuilder.durable(singleQueueName)
                .withArgument("x-dead-letter-exchange", deadLetterExchange)
                .withArgument("x-dead-letter-routing-key", "dead-letter-key")
                .withArgument("x-message-ttl", 60000) // 消息1分钟未消费进入死信队列
                .build();
    }

    // 3. 声明群聊队列(绑定死信交换机)
    @Bean
    public Queue groupMessageQueue() {
        return QueueBuilder.durable(groupQueueName)
                .withArgument("x-dead-letter-exchange", deadLetterExchange)
                .withArgument("x-dead-letter-routing-key", "dead-letter-key")
                .withArgument("x-message-ttl", 60000)
                .build();
    }

    // 4. 声明主题交换机
    @Bean
    public TopicExchange messageExchange() {
        return ExchangeBuilder.topicExchange(exchangeName).durable(true).build();
    }

    // 5. 绑定单聊队列与交换机
    @Bean
    public Binding singleMessageBinding() {
        return BindingBuilder.bind(singleMessageQueue()).to(messageExchange()).with("message.single.#");
    }

    // 6. 绑定群聊队列与交换机
    @Bean
    public Binding groupMessageBinding() {
        return BindingBuilder.bind(groupMessageQueue()).to(messageExchange()).with("message.group.#");
    }

    // 7. 配置RabbitTemplate(生产者确认)
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        // 生产者确认回调
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                log.info("消息发送到交换机成功,correlationId: {}", correlationData.getId());
            } else {
                log.error("消息发送到交换机失败,cause: {}", cause);
            }
        });
        // 消息返回回调(交换机无法路由到队列时触发)
        rabbitTemplate.setReturnsCallback(returned -> {
            log.error("消息路由失败,exchange: {}, routingKey: {}, message: {}",
                    returned.getExchange(), returned.getRoutingKey(), returned.getMessage());
        });
        return rabbitTemplate;
    }
}

2.4 核心业务实现:消息发送与消费

2.4.1 消息发送服务(MessageSendService.java)
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.google.protobuf.InvalidProtocolBufferException;
import com.im.config.RabbitMQConfig;
import com.im.entity.Message;
import com.im.protobuf.MessageProto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@Service
@Slf4j
@RequiredArgsConstructor
public class MessageSendService {

    private final RabbitTemplate rabbitTemplate;
    private final StringRedisTemplate redisTemplate;

    @Value("${im.rabbitmq.exchange-name}")
    private String exchangeName;

    @Value("${im.redis.unread-prefix}")
    private String unreadPrefix;

    /**
     * 发送单聊消息
     */
    public void sendSingleMessage(Long senderId, Long receiverId, Integer type, String content) {
        // 1. 生成全局唯一消息ID(雪花算法)
        Long messageId = IdWorker.getId();

        // 2. 构建消息实体
        Message message = new Message();
        message.setId(messageId);
        message.setSenderId(senderId);
        message.setReceiverId(receiverId);
        message.setType(type);
        message.setContent(content);
        message.setStatus(0); // 待送达
        message.setSendTime(LocalDateTime.now());

        // 3. Protobuf序列化
        MessageProto.Message protoMessage = MessageProto.Message.newBuilder()
                .setId(messageId)
                .setSenderId(senderId)
                .setReceiverId(receiverId)
                .setType(type)
                .setContent(content)
                .setSendTime(message.getSendTime().toString())
                .build();
        byte[] messageBytes = protoMessage.toByteArray();

        // 4. 发送到RabbitMQ(单聊路由键)
        rabbitTemplate.convertAndSend(
                exchangeName,
                "message.single.send",
                messageBytes,
                correlationData -> {
                    correlationData.setId(messageId.toString());
                    return correlationData;
                }
        );

        // 5. 预存Redis未读列表(提升响应速度)
        String unreadKey = unreadPrefix + receiverId;
        redisTemplate.opsForList().leftPush(unreadKey, messageId.toString());
        log.info("单聊消息发送成功,messageId: {}, senderId: {}, receiverId: {}", messageId, senderId, receiverId);
    }
}
2.4.2 消息消费服务(MessageConsumer.java)
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.google.protobuf.InvalidProtocolBufferException;
import com.im.entity.Message;
import com.im.mapper.MessageMapper;
import com.im.protobuf.MessageProto;
import com.im.service.WebSocketService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
@Slf4j
@RequiredArgsConstructor
public class MessageConsumer {

    private final MessageMapper messageMapper;
    private final StringRedisTemplate redisTemplate;
    private final WebSocketService webSocketService;

    /**
     * 消费单聊消息
     */
    @RabbitListener(queues = "${im.rabbitmq.single-queue-name}")
    @Transactional(rollbackFor = Exception.class)
    public void consumeSingleMessage(byte[] messageBytes, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
                                    @Header(AmqpHeaders.MESSAGE_ID) String messageId,
                                    org.springframework.amqp.core.Channel channel) {
        try {
            // 1. Protobuf反序列化
            MessageProto.Message protoMessage = MessageProto.Message.parseFrom(messageBytes);
            
            // 2. 构建数据库存储实体
            DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
            LocalDateTime sendTime = LocalDateTime.parse(protoMessage.getSendTime(), formatter);
            
            Message message = new Message();
            message.setId(protoMessage.getId());
            message.setSenderId(protoMessage.getSenderId());
            message.setReceiverId(protoMessage.getReceiverId());
            message.setType(protoMessage.getType());
            message.setContent(protoMessage.getContent());
            message.setStatus(1); // 已送达
            message.setSendTime(sendTime);
            message.setDeliverTime(LocalDateTime.now());

            // 3. 幂等性校验(避免重复消费)
            if (messageMapper.selectById(message.getId()) != null) {
                log.warn("消息已消费,跳过重复处理,messageId: {}", message.getId());
                channel.basicAck(deliveryTag, false);
                return;
            }

            // 4. 持久化到MySQL
            messageMapper.insert(message);
            log.info("消息持久化成功,messageId: {}", message.getId());

            // 5. 更新Redis会话缓存(最新消息)
            String sessionKey = "${im.redis.session-prefix}" + message.getReceiverId() + ":" + message.getSenderId();
            redisTemplate.opsForHash().putAll(sessionKey, Map.of(
                    "lastMessageId", message.getId().toString(),
                    "lastContent", message.getContent(),
                    "lastSendTime", message.getSendTime().toString(),
                    "unreadCount", redisTemplate.opsForList().size("${im.redis.unread-prefix}" + message.getReceiverId())
            ));

            // 6. WebSocket实时推送消息给接收方
            webSocketService.pushMessageToUser(message.getReceiverId(), message);

            // 7. 手动ACK确认消息消费成功
            channel.basicAck(deliveryTag, false);
        } catch (InvalidProtocolBufferException e) {
            log.error("消息反序列化失败,messageId: {}", messageId, e);
            // 序列化失败直接拒绝,进入死信队列
            rejectMessage(channel, deliveryTag);
        } catch (Exception e) {
            log.error("消息消费异常,messageId: {}", messageId, e);
            // 非序列化异常重试3次后进入死信队列
            retryOrReject(channel, deliveryTag, messageId);
        }
    }

    /**
     * 消息拒绝处理
     */
    private void rejectMessage(org.springframework.amqp.core.Channel channel, long deliveryTag) {
        try {
            channel.basicReject(deliveryTag, false);
        } catch (Exception e) {
            log.error("消息拒绝失败", e);
        }
    }

    /**
     * 重试或拒绝消息
     */
    private void retryOrReject(org.springframework.amqp.core.Channel channel, long deliveryTag, String messageId) {
        // 获取消息重试次数(从Redis记录)
        String retryKey = "im:message:retry:" + messageId;
        Integer retryCount = Integer.parseInt(redisTemplate.opsForValue().getOrDefault(retryKey, "0"));
        
        if (retryCount < 3) {
            // 重试次数未满,重新入队
            try {
                redisTemplate.opsForValue().set(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
                channel.basicNack(deliveryTag, false, true);
            } catch (Exception e) {
                log.error("消息重新入队失败", e);
            }
        } else {
            // 重试次数已满,拒绝进入死信队列
            rejectMessage(channel, deliveryTag);
            redisTemplate.delete(retryKey);
        }
    }
}

2.5 已读状态同步实现

2.5.1 已读状态服务(ReadStatusService.java)
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Service
@Slf4j
@RequiredArgsConstructor
public class ReadStatusService {

    private final MessageMapper messageMapper;
    private final StringRedisTemplate redisTemplate;

    @Value("${im.redis.read-prefix}")
    private String readPrefix;

    @Value("${im.redis.unread-prefix}")
    private String unreadPrefix;

    @Value("${im.redis.session-prefix}")
    private String sessionPrefix;

    /**
     * 标记消息为已读(缓存优先)
     */
    public void markMessageAsRead(Long userId, Long senderId, List<Long> messageIds) {
        if (CollectionUtils.isEmpty(messageIds)) {
            return;
        }

        LocalDateTime readTime = LocalDateTime.now();
        String unreadKey = unreadPrefix + userId;
        String sessionKey = sessionPrefix + userId + ":" + senderId;
        String unsyncReadKey = readPrefix + userId;

        // 1. 从Redis未读列表中移除已读消息ID
        messageIds.forEach(messageId -> redisTemplate.opsForList().remove(unreadKey, 0, messageId.toString()));

        // 2. 更新会话未读计数
        Long unreadCount = redisTemplate.opsForList().size(unreadKey);
        redisTemplate.opsForHash().put(sessionKey, "unreadCount", unreadCount.toString());

        // 3. 记录未同步到数据库的已读消息(批量同步用)
        messageIds.forEach(messageId -> redisTemplate.opsForZSet().add(unsyncReadKey, messageId.toString(), readTime.toEpochSecond(java.time.ZoneOffset.UTC)));

        // 4. 多端同步:发布已读状态广播(其他在线设备监听)
        redisTemplate.convertAndSend("im:read:broadcast", Map.of(
                "userId", userId,
                "senderId", senderId,
                "messageIds", messageIds,
                "readTime", readTime
        ));

        log.info("标记消息已读(缓存),userId: {}, messageIds: {}", userId, messageIds);
    }

    /**
     * 定时批量同步已读状态到MySQL(每5分钟执行)
     */
    @Scheduled(cron = "0 0/5 * * * ?")
    @Transactional(rollbackFor = Exception.class)
    public void syncReadStatusToDB() {
        // 1. 获取所有未同步的已读消息Key
        Set<String> unsyncReadKeys = redisTemplate.keys(readPrefix + "*");
        if (CollectionUtils.isEmpty(unsyncReadKeys)) {
            return;
        }

        for (String key : unsyncReadKeys) {
            Long userId = Long.parseLong(key.replace(readPrefix, ""));
            // 2. 获取该用户所有未同步的已读消息ID
            Set<String> messageIdStrs = redisTemplate.opsForZSet().range(key, 0, -1);
            if (CollectionUtils.isEmpty(messageIdStrs)) {
                redisTemplate.delete(key);
                continue;
            }

            // 3. 转换为Long类型列表
            List<Long> messageIds = messageIdStrs.stream()
                    .map(Long::valueOf)
                    .toList();

            // 4. 批量更新数据库状态
            int updateCount = messageMapper.batchUpdateReadStatus(messageIds, LocalDateTime.now());
            log.info("批量同步已读状态到DB,userId: {}, 同步数量: {}", userId, updateCount);

            // 5. 同步成功后删除Redis中的未同步记录
            redisTemplate.delete(key);
        }
    }
}

2.6 历史消息查询实现

2.6.1 历史消息服务(HistoryMessageService.java)
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
@RequiredArgsConstructor
public class HistoryMessageService {

    private final MessageMapper messageMapper;
    private final StringRedisTemplate redisTemplate;

    @Value("${im.redis.session-prefix}")
    private String sessionPrefix;

    /**
     * 分页查询历史消息(缓存优先)
     */
    public List<Message> queryHistoryMessage(Long userId, Long targetId, Long lastId, Integer pageSize) {
        // 1. 先查Redis缓存(近7天热点消息)
        String cacheKey = "im:history:" + userId + ":" + targetId;
        List<String> cachedMessageIds = redisTemplate.opsForList().range(cacheKey, 0, -1);
        
        if (CollectionUtils.isNotEmpty(cachedMessageIds)) {
            List<Long> messageIds = cachedMessageIds.stream().map(Long::valueOf).toList();
            // 过滤出小于lastId的消息(分页加载)
            List<Long> targetIds = messageIds.stream()
                    .filter(id -> lastId == null || id < lastId)
                    .limit(pageSize)
                    .toList();
            
            if (CollectionUtils.isNotEmpty(targetIds)) {
                List<Message> cachedMessages = messageMapper.selectBatchIds(targetIds);
                // 按消息ID倒序排列(最新的在前)
                cachedMessages.sort((m1, m2) -> m2.getId().compareTo(m1.getId()));
                return cachedMessages;
            }
        }

        // 2. 缓存未命中,查询MySQL
        List<Message> dbMessages = messageMapper.queryHistoryMessage(
                userId, targetId, lastId, pageSize
        );

        // 3. 结果写入缓存(有效期7天)
        if (CollectionUtils.isNotEmpty(dbMessages)) {
            List<String> idStrs = dbMessages.stream()
                    .map(message -> message.getId().toString())
                    .toList();
            redisTemplate.opsForList().rightPushAll(cacheKey, idStrs);
            redisTemplate.expire(cacheKey, 7, java.util.concurrent.TimeUnit.DAYS);
        }

        log.info("查询历史消息完成,userId: {}, targetId: {}, 数量: {}", userId, targetId, dbMessages.size());
        return dbMessages;
    }
}

2.7 WebSocket 实时推送配置

2.7.1 WebSocket 配置(WebSocketConfig.java)
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import javax.annotation.Resource;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Resource
    private WebSocketHandler webSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册WebSocket端点,允许跨域
        registry.addHandler(webSocketHandler, "/ws/im/{userId}")
                .setAllowedOrigins("*");
    }
}
2.7.2 WebSocket 处理器(WebSocketHandler.java)
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Slf4j
public class WebSocketHandler extends TextWebSocketHandler {

    // 存储用户ID与WebSocketSession的映射
    private static final Map<Long, WebSocketSession> USER_SESSIONS = new ConcurrentHashMap<>();

    /**
     * 连接建立成功
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 从URL路径获取用户ID
        String userIdStr = session.getUri().getPath().split("/")[3];
        Long userId = Long.parseLong(userIdStr);
        USER_SESSIONS.put(userId, session);
        log.info("WebSocket连接建立,userId: {}, 在线用户数: {}", userId, USER_SESSIONS.size());
    }

    /**
     * 连接关闭
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        Long userId = USER_SESSIONS.entrySet().stream()
                .filter(entry -> entry.getValue().equals(session))
                .map(Map.Entry::getKey)
                .findFirst()
                .orElse(null);
        
        if (userId != null) {
            USER_SESSIONS.remove(userId);
            log.info("WebSocket连接关闭,userId: {}, 在线用户数: {}", userId, USER_SESSIONS.size());
        }
    }

    /**
     * 推送消息给指定用户
     */
    public void pushMessageToUser(Long userId, Object message) {
        WebSocketSession session = USER_SESSIONS.get(userId);
        if (session != null && session.isOpen()) {
            try {
                String jsonMessage = JSON.toJSONString(message);
                session.sendMessage(new TextMessage(jsonMessage));
                log.info("推送消息给用户成功,userId: {}, message: {}", userId, jsonMessage);
            } catch (Exception e) {
                log.error("推送消息给用户失败,userId: {}", userId, e);
            }
        } else {
            log.warn("用户未在线,消息暂存,userId: {}", userId);
            // 离线用户消息可存入Redis,待上线后拉取
        }
    }
}

2.8 Protobuf 协议定义(message.proto)

syntax = "proto3";

package com.im.protobuf;

option java_package = "com.im.protobuf";
option java_outer_classname = "MessageProto";

// 消息协议定义
message Message {
  int64 id = 1; // 消息ID
  int64 senderId = 2; // 发送者ID
  int64 receiverId = 3; // 接收者ID
  int32 type = 4; // 消息类型:1-文本,2-图片,3-语音
  string content = 5; // 消息内容
  string sendTime = 6; // 发送时间(ISO格式)
}

三、可靠性与性能优化关键措施

3.1 可靠性保障

  1. 消息不丢失:RabbitMQ 消息持久化 + 生产者确认 + 消费者手动 ACK,死信队列处理失败消息;
  2. 幂等性处理:消息 ID 作为唯一键,MySQL 主键约束 + Redis 去重,避免重复消费;
  3. 数据一致性:缓存与数据库双写,定时对账任务校验 Redis 与 MySQL 数据差异;
  4. 容灾备份:MySQL 主从复制,Redis 主从 + 哨兵模式,RabbitMQ 集群部署。

3.2 性能优化

  1. 数据库优化:批量插入 / 更新、联合索引、分表分库(按接收者 ID 分片);
  2. 缓存优化:热点数据预加载、LRU 淘汰策略、缓存穿透防护(空值缓存);
  3. 异步优化:核心流程异步化,非关键操作(如日志记录)@Async 异步执行;
  4. 序列化优化:Protobuf 替代 JSON,减少 40% 传输体积,提升序列化效率。

四、系统扩展方向

  1. 功能扩展:支持图片 / 语音消息(MinIO 存储文件)、消息撤回、群聊 @功能、消息搜索(Elasticsearch);
  2. 架构扩展:服务拆分(单聊 / 群聊独立服务)、Redis 集群分片、RabbitMQ 队列拆分;
  3. 体验优化:离线消息同步、消息已回执、多端登录状态同步。

五、总结

本文基于 SpringBoot+RabbitMQ+Redis+MySQL 技术栈,实现了一套 “高性能、高可靠、可扩展” 的私信系统,核心亮点在于 “异步解耦 + 缓存优先 + 可靠投递” 的设计思路。提供的代码可直接复用,只需根据实际业务调整配置(如数据库连接、队列名称)即可快速落地。