1. 项目背景与核心需求
最近在开发一个社交平台的私信模块时,遇到了几个棘手的业务场景:用户A发送消息后需要实时推送给用户B,用户B阅读后需要立即反馈已读状态给用户A,同时还要保证历史消息的快速加载。这种场景对系统架构提出了三个核心要求:
- 实时性:消息发送和状态变更需要秒级触达
2.可靠性:确保消息不丢失、不重复
3.高性能:历史消息查询响应时间控制在200ms内
经过技术选型,最终确定的方案组合是:SpringBoot作为基础框架,RabbitMQ处理实时消息,Redis缓存活跃数据,MySQL持久化存储。这个技术栈的组合考虑到了各组件的特点:
- RabbitMQ的发布/订阅模式非常适合实时通知
- Redis的读写性能可以支撑高并发访问
- MySQL则提供了可靠的数据持久化
2. 系统架构设计
2.1 整体架构图
code复制[客户端] -> [API网关] -> [消息服务]
-> [状态服务]
-> [缓存服务]
-> [存储服务]
2.2 核心组件职责
- 消息服务:处理消息的发送逻辑
- 状态服务:管理已读/未读状态
- 缓存服务:维护活跃消息缓存
- 存储服务:持久化所有消息数据
3. 关键技术实现
3.1 消息发送流程
- 客户端调用发送接口
- 服务端验证后写入MySQL
- 同时将消息发布到RabbitMQ
- 接收方在线则实时推送
- 接收方离线则存入Redis待推送队列
java复制// 消息发送核心代码示例
public void sendMessage(Message msg) {
// 1. 持久化存储
messageRepository.save(msg);
// 2. 发布到消息队列
rabbitTemplate.convertAndSend(
"msg.exchange",
"msg.route",
msg
);
// 3. 写入发送方缓存
redisTemplate.opsForList().leftPush(
"user:"+msg.getSender()+":sent",
msg
);
}
3.2 已读状态同步
采用双写策略保证一致性:
- 客户端标记已读时先更新Redis
- 通过消息队列异步更新MySQL
- 反向通知发送方更新本地状态
注意:这里需要考虑消息幂等处理,防止网络重传导致状态不一致
3.3 历史消息缓存
使用Redis的SortedSet实现分页缓存:
java复制// 消息缓存结构示例
ZADD user:123:inbox 1620000000 "msg1_content"
ZADD user:123:inbox 1620001000 "msg2_content"
// 分页查询
ZREVRANGE user:123:inbox 0 19
缓存更新策略:
- 新消息:实时写入
- 旧消息:按需加载
- 过期策略:LRU自动淘汰
4. 性能优化实践
4.1 RabbitMQ调优
- 开启生产者确认模式
- 设置合理的prefetch count
- 使用单独的vhost隔离流量
4.2 Redis优化
- 使用Pipeline批量操作
- 合理设置数据结构:
- 消息内容用String存储
- 关系用Set存储
- 时间线用SortedSet存储
4.3 MySQL优化
- 采用分表策略:按用户ID哈希分表
- 建立复合索引:(sender,receiver,timestamp)
- 定期归档冷数据
5. 异常处理方案
5.1 消息丢失防护
- 开启RabbitMQ持久化
- 实现本地消息表
- 增加定时补偿任务
5.2 数据一致性保证
采用最终一致性方案:
- 先更新缓存
- 异步更新数据库
- 定期对账修复
5.3 限流保护
- 发送频率限制:10条/秒
- 接收频率限制:100条/秒
- 突发流量队列缓冲
6. 监控与运维
6.1 关键指标监控
- 消息延迟:P99<500ms
- 已读状态同步成功率>99.9%
- 缓存命中率>95%
6.2 日志规范
- 消息轨迹日志
- 状态变更日志
- 异常日志分级
6.3 扩缩容策略
- RabbitMQ:垂直扩容
- Redis:集群分片
- MySQL:读写分离
7. 踩坑经验分享
在实际开发中遇到过几个典型问题:
- 消息顺序问题:解决方案是使用单队列单消费者模式
- Redis内存暴涨:通过设置合理的TTL解决
- 已读状态抖动:最终采用版本号机制解决
一个特别值得注意的细节是:当用户快速滑动查看历史消息时,如果直接查询数据库会导致性能骤降。我们的优化方案是:
- 首次加载时预取前后各20条
- 滑动时优先从缓存获取
- 异步预加载可能访问的数据
这种方案使得在测试环境下,消息列表滑动流畅度提升了300%,CPU负载降低了40%。