基于 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 可靠性保障
- 消息不丢失:RabbitMQ 消息持久化 + 生产者确认 + 消费者手动 ACK,死信队列处理失败消息;
- 幂等性处理:消息 ID 作为唯一键,MySQL 主键约束 + Redis 去重,避免重复消费;
- 数据一致性:缓存与数据库双写,定时对账任务校验 Redis 与 MySQL 数据差异;
- 容灾备份:MySQL 主从复制,Redis 主从 + 哨兵模式,RabbitMQ 集群部署。
3.2 性能优化
- 数据库优化:批量插入 / 更新、联合索引、分表分库(按接收者 ID 分片);
- 缓存优化:热点数据预加载、LRU 淘汰策略、缓存穿透防护(空值缓存);
- 异步优化:核心流程异步化,非关键操作(如日志记录)@Async 异步执行;
- 序列化优化:Protobuf 替代 JSON,减少 40% 传输体积,提升序列化效率。
四、系统扩展方向
- 功能扩展:支持图片 / 语音消息(MinIO 存储文件)、消息撤回、群聊 @功能、消息搜索(Elasticsearch);
- 架构扩展:服务拆分(单聊 / 群聊独立服务)、Redis 集群分片、RabbitMQ 队列拆分;
- 体验优化:离线消息同步、消息已回执、多端登录状态同步。
五、总结
本文基于 SpringBoot+RabbitMQ+Redis+MySQL 技术栈,实现了一套 “高性能、高可靠、可扩展” 的私信系统,核心亮点在于 “异步解耦 + 缓存优先 + 可靠投递” 的设计思路。提供的代码可直接复用,只需根据实际业务调整配置(如数据库连接、队列名称)即可快速落地。