消息存储子服务,在整个即时通讯微服务架构里属于那种"平时没人夸、一出事全找它"的模块。我当年接手这套系统的时候,线上消息量已经过亿,单库单表扛到了极限,每次大促或者晚高峰,消息拉取接口的耗时就像过山车。后来我们把消息存储拆成独立子服务,重构了数据模型和写入链路,才把问题压下去。这篇文章就把我做这个子服务的完整思路、踩坑过程和最终的落地方案整理出来,给准备做或者正在做IM微服务拆分的同学一个参考。
整个系列会分成多篇来讲,这篇聚焦在消息存储子服务1,也就是整体定位、数据模型、写入链路和查询链路的第一次设计迭代。后面还会聊到分库分表、多端同步位点、冷热数据分离,以及消息回溯和审计相关的进阶内容。
1. 为什么消息存储要单独拆成一个子服务
很多人一开始做IM系统,习惯性把消息存储直接塞进业务服务里,聊天接口里顺手写一条数据库记录就完了。这套逻辑在用户量小、消息量可控的时候没什么问题,但一旦聊天的频率上去了,第一个扛不住的就是数据库连接池和锁竞争。消息写入的高峰往往是突发的,比如一个万人群同时刷屏,或者直播间的公聊,瞬间的写QPS能冲到几千甚至上万,如果这个写压力直接打在业务服务的主库上,聊天接口和用户登录、资料修改这些核心接口就会互相拖垮。
把消息存储拆成独立子服务,核心收益有三个:
第一个是故障隔离。业务服务还在线,但消息存储服务因为磁盘故障或者慢查询挂了,用户可以继续收发信令,只是消息记录暂时查不到,等存储恢复后再补同步。这比整个聊天功能全部雪崩要好得多。
第二个是水平扩展。存储服务可以独立增加副本和分片,根据消息量级别单独做容量规划。你不用为了应付消息量的增长,给整个业务集群全部扩容,成本控制更灵活。
第三个是查询和写入的解耦。存储子服务可以同时服务多个上游业务线,比如App聊天、Web客服、系统通知,这些业务共享一套消息存取能力,各自的逻辑只需要封装成一两句调用就行。
我见过有些团队偷懒,把消息存储做成业务服务的一个模块,用Feign或RPC接口对外暴露,但底层还是同一个数据库。这不叫拆分,只是把接口换了个皮。真正的独立子服务,从数据表、连接池、存储介质到部署单元都应该是独立的,至少要做到数据库层面的物理隔离。否则核心消息库出现慢查询或锁等待,一样会把整个业务集群拖下水。当时我们设计的时候,直接给消息存储服务配了独立的一组MySQL实例,以及一套单独的Redis集群,内存不跟业务服务混用,从根上避免互相干扰。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 消息存储子服务的边界划分与功能清单
拆服务不是拆完就完了,最重要的先想清楚这个服务"管什么、不管什么"。边界不清晰,后面协作的时候必然扯皮,接口设计也会反复改。
我们这个消息存储子服务最终划定的边界是这样的。它只负责消息数据本身的持久化、读取、归档与删除,不负责任何消息推送逻辑、不持有用户的长连接,也不做消息内容的全文检索。全文检索我们独立交给了ES集群,消息存储只服务ES的数据回源。这样可以避免存储服务承担太多职责,导致迭代时互相影响。
功能清单大概列一下:
- 消息落库:各类消息的异步写入,包括单聊、群聊、系统通知,以及消息内容的版本历史。
- 消息拉取:按会话维度拉取历史记录,支持分页、游标、按时间段过滤。
- 会话维度管理:获取一个会话最新的N条消息、未读数相关的消息位置信息。
- 多端同步位点:记录每个用户每个设备的同步游标,配合增量拉取。
- 消息撤回与删除:标记删除消息,支持撤回后的留痕与占位展示。
- 归档与转储:冷数据定期迁移到归档存储,保证热库查询性能。
有些团队会把"消息已读回执"也放进存储服务,但我们在做架构评审的时候否决了。已读回执本质上是状态机,更新频率比消息本身高得多,写入量更大,和消息append-only的特性完全不一样。把它硬塞进存储子服务,会导致一张消息表既有追加写又有高频更新,锁竞争翻倍。所以我们把已读回执拆给了独立的在线状态服务,消息存储只维护这条消息是否被撤回、是否被删除这些静态状态。
这个边界划分在实际协作中效果很好。客户端团队只需要对接消息拉取和发送时调用存储接口,不需要关心消息是怎么存进去的;推送服务需要消息内容做推送文案时,也是通过存储服务拿数据,但存储服务不反向调用推送。依赖方向是单向的,整个链路很清爽。
3. 数据表设计:核心表结构、索引策略与扩展预留
边界定清楚以后,开始设计表结构。消息存储子服务的表结构比很多人想象中要简单,核心就三张表,但每张表的字段和索引都需要反复推敲,因为这张表一旦上线运行,后面改起来成本极高。
先看核心的消息表。结构大体如下:
sql复制CREATE TABLE `im_message_0` (
`msg_id` bigint(20) NOT NULL COMMENT '全局消息ID,雪花算法生成',
`conversation_id` bigint(20) NOT NULL COMMENT '会话ID,单聊双人会话和群聊会话统一编码',
`sender_id` bigint(20) NOT NULL COMMENT '发送者用户ID',
`msg_type` tinyint(4) NOT NULL COMMENT '消息类型:1文本 2图片 3语音 4视频 5文件 6系统 7撤回占位',
`content` text NOT NULL COMMENT '消息内容,文本或媒体文件的URL/对象信息',
`client_msg_id` varchar(64) NOT NULL COMMENT '客户端生成的消息唯一ID,用于幂等去重',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0正常 1撤回 2删除',
`create_time` datetime(3) NOT NULL COMMENT '消息产生时间',
`update_time` datetime(3) NOT NULL COMMENT '最后更新时
