本文目录导读:

设计一个扩展性强的站内信系统,核心挑战在于平衡实时性、存储成本和海量读写,以下是一套经过实战验证的架构设计方案,重点考虑了水平扩展和业务隔离。
核心架构原则:读写分离与数据分片
- 读写分离:将“发送”和“查询”操作彻底分开,使用不同的数据表甚至不同的数据库。
- 数据分片:按照用户ID或会话ID进行水平分片(Sharding),避免单表数据量过大。
- 冷热分离:近期活跃数据(如最近30天)与历史归档数据分开存储。
数据结构设计
传统的“收件箱+发件箱”架构在扩展时容易遇到瓶颈,推荐采用 “消息-关系-会话” 三层模型,这样更有利于扩展。
消息层(Message Store)
- 表结构:
message_id (bigint, 自增/分布式ID),sender_id,content,msg_type,created_at,metadata (JSON) - 特点:只存储一份,所有收件人都指向同一条message_id,内容不冗余,节省大量存储。
关系层(User-Message Relation)
- 表结构:
relation_id (bigint),user_id (分区键),message_id,box_type (0:收件箱, 1:发件箱),status (0:未读, 1:已读, 2:已删除, 3:已撤回),is_starred (bit),batch_id (用于群发标记) - 分区键:必须用user_id作为分区键(Sharding Key),这样单用户的所有信件都落在同一节点,方便分页查询。
- 索引设计:需要
(user_id, status, created_at)作为联合索引,覆盖站内信最常见的“查未读消息”场景。
会话层(可选,用于聚合)
- 如果需要类似“对话”的效果,可以增加
conversation_id字段,在关系层或专门建立会话表,将同一主题的消息聚合展示。
关键扩展机制
群发与系统通知:写扩散 vs 读扩散
- 写扩散:给100万用户群发,会生成100万条relation记录,但用户的“消息已读”状态可以单独更新,不会影响其他用户。
- 读扩散:不生成relation记录,用户登录时实时按条件查询(如
sender_id in (system)),适合全量群发,但查询压力大。 - 最佳实践:混合策略:
- 对于普通群发(<5000人),采用写扩散。
- 对于全量系统公告(如版本更新),采用读扩散 + 时间戳过滤,例如
user_message_view表记录用户最后阅读系统公告的时间,前端只展示晚于该时间的公告。
消息已读/未读查询优化
- 痛点:群发消息要查“多少人已读”,如果用
count(*)会非常慢。 - 解决方案:使用Redis Bitmap或Bloom Filter记录已读状态,例如对每一条群发消息,用一个Redis key
msg_<msg_id>_read,用bit位标记用户ID(需要建立用户ID到bit位的映射函数),查询已读数时直接bitcount。
分页优化
- 传统方案:
SELECT * FROM relation WHERE user_id=? ORDER BY created_at DESC LIMIT 20 OFFSET 1000 - 问题:深分页导致性能急剧下降。
- 优化方案:基于游标(Cursor)的分页,例如
WHERE user_id=? AND created_at < last_created_at ORDER BY created_at DESC LIMIT 20,前端传回当前列表最后一条消息的ID或时间戳,后端直接用索引定位,无论翻到第几页,性能都恒定。
存储层扩展方案
分库分表策略
- 水平分表:按
user_id分16/64/256张表(或更多),可以使用user_id % shard_count。 - 数据一致性:基于
user_id的查询都在同一分片,天然避免跨分片事务。 - 跨分片查询:全局搜索功能(如管理员查所有用户的某条消息)需要走ES(Elasticsearch)或OLAP(联机分析处理)引擎,不直接在关系库做。
冷热分离
- 热数据:最近30天活跃消息,放在高性能磁盘(如NVMe SSD)或TiDB等HTAP(混合事务/分析处理)数据库。
- 冷数据:超过30天的消息,自动迁移到低成本存储,如AWS S3、阿里云OSS,或性价比高的HDD机器,迁移策略可以使用定时任务
DELETE + INSERT INTO cold_table或直接用分区表PARTITION BY RANGE (created_at)。
实时性与推送
- 在线推送:用户在线时,通过WebSocket直接推送到客户端,并更新小红点。
- 离线积累:用户不在线时,消息存储在DB中,用户下次登录时,前端轮询
GET /api/inbox/unread_count?last_poll_time=xxx接口。 - 消息计数器:用Redis维护每个用户的未读消息计数器,key为
unread:user_id,值用int(整型),有消息入站时incr,用户查看后decr或直接重新set。
扩展性提升技巧
| 关注点 | 具体做法 | 好处 |
|---|---|---|
| ID生成 | 使用雪花算法(Snowflake)或Leaf ID,保证全局唯一且有序 | 利于分页和索引 |
| 异步处理 | 群发消息先入MQ(消息队列,如Kafka),消费端批量写入关系表 | 削峰填谷,保护数据库 |
| ES搜索 | 同步至ES | 支持全文检索和高级过滤 |
| 多租户隔离 | 在relation表中加入tenant_id(租户ID)字段 |
支持同一套代码服务多个业务线 |
| 限流与降级 | 针对发消息接口做令牌桶限流;当DB压力过大时,降级为“消息发送成功后异步落库” | 防止意外流量冲垮系统 |
一个典型的写入流程(群发1万用户)
- API接收:
POST /api/message/send、收件人列表、发送者。 - 写入消息表:生成
message_id,写入message。 - 发送MQ:封装
{message_id, user_ids: [1,2,...,10000]}到Kafka Topic “inbox_write”。 - 消费端处理:
- 按用户ID%分片数,将relation数据写入不同的Shard表。
- 同时更新Redis计数器
unread:user_id(对每个用户做INCR操作,批量合并时用INCRBY)。
- WebSocket推送:如果用户在线,通知其更新小红点数量。
推荐技术栈
- 数据库:TiDB(HTAP自动水平扩展)或 ShardingSphere + MySQL(分库分表)。
- 缓存:Redis Cluster(存未读计数、会话热点数据)。
- 消息队列:Pulsar / Kafka(处理群发、同步ES)。
- 搜索引擎:Elasticsearch(全文检索)。
- 存储:对象存储(腾讯云COS、OSS) + CDN(内容分发网络)(存储图片、附件、历史归档)。
需要避开的坑
- *不要用`SELECT COUNT()`频繁查未读数**:用Redis实时计数代替。
- 不要直接删除消息:逻辑删除(
status=2),物理删除用定时任务清理。 - 不要一次性查询全部收件人:即使只有100个收件人,也要分100次缓存写入或批量INSERT(插入)。
- 避免大事务:一次群发写入如果用
INSERT ... VALUES (10000 rows),会锁资源,应拆分成批量(每次500-1000条)提交。
这套设计能支撑千万级用户、每天上亿条消息的站内信系统,且关键操作(发、收、翻页)都可以通过增加节点线性扩展,你需要根据实际业务流量修正分片数和缓存策略。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播