你现在的位置:首页 > APP开发 > 社交交友类APP > 正文

消息推送的已读回执,如何避免DB暴增?

发布时间:2026-05-27    来源:     作者:    阅读:

在消息推送系统中,“已读回执”是一项常见功能。它能够让消息发送方获知接收方是否已查看某条消息,从而支持业务闭环或社交互动。然而,随着用户量和消息量增长,已读回执数据会快速膨胀——每条消息针对每个接收方都可能产生一条独立的回执记录,导致数据库(DB)表规模急剧增加,带来存储成本、写入压力以及查询性能三方面的挑战。本文将从问题本质出发,系统性地探讨如何避免已读回执导致数据库暴增,给出若干工程化策略,并分析其适用场景与权衡。

一、已读回执数据量暴增的本质原因

在典型实现中,每条消息的已读回执往往被存储为一条独立记录,常见结构为(消息ID,接收方ID,已读状态,已读时间戳)。当一条消息发送给N个接收方时,理想情况下会生成N条回执记录。如果系统每天产生M条消息,平均群发规模为K,则每日新增回执记录数约为 M × K。对于千万级DAU的系统,M和K数值可能都非常可观,一天新增数亿条回执记录并不罕见。

这种存储模式本质上是在为每一个(消息,接收方)二元组维护状态。如果不加干预,数据库将在数周或数月内膨胀至数十亿甚至数百亿行,严重影响系统稳定性。

二、避免DB暴增的核心策略

要解决这一问题,不能单纯依赖数据库的硬件扩容,而必须从数据模型、写入方式、存储介质与数据生命周期管理等多个层面进行综合设计。以下逐一展开。

1. 数据冷热分离

已读回执数据具有明显的“冷热”特征:近期产生的回执(例如最近7天或30天)被频繁查询和更新;而超过一定时间的历史回执几乎只用于极少数的“回溯”场景,甚至完全不再被使用。因此,最直接有效的方法是将热数据与冷数据分离存储。

具体做法是:

  • 热数据表:只保留最近一段时间(如30天)的回执记录,存储在高性能SSD数据库中,支持高并发读写。

  • 冷数据表或归档区:将超过30天的回执记录迁移到廉价存储介质(如HDD、对象存储或列式存储系统)中,可以按日期分区表进行归档。

为了降低业务代码的复杂度,可采用“分区表+自动分区交换”的方式:每天创建新的分区,30天前的分区整体剥离到归档表或转换为只读分区。查询时,应用层通常只查询热数据分区;若需查询更早数据,则路由到归档存储。

效果:热数据表规模被控制在固定时间段内,避免了无限增长。例如每日新增1亿条回执,保留30天,则热表稳定在30亿行——虽然仍然很大,但若配合下文其他策略,行数可以进一步压缩。

2. 数据聚合与去冗余

并非所有回执都需要独立存储。根据业务对回执粒度的要求,可以采用多种聚合手段。

2.1 按会话聚合

在群聊场景中,用户往往只需要知道“某条消息是否已被群内部分成员阅读”,而不必为每一个群成员单独记录精细化的已读状态。此时可以采用“已读人数计数器”替代明细:

  • 每条群聊消息存储一个“已读计数”字段(存储在消息表或单独的计数表中)。

  • 当一名用户阅读该消息时,原子性地增加计数(使用Redis或数据库行锁优化)。同时,可以记录最后几名已读用户的信息(用于UI显示“张三、李四等5人已读”)。

  • 如果业务需要精确知道某一特定用户是否已读某条消息,则可以在内存或NoSQL中维护短期状态,过期后转为只保留是否已读过的聚合信息。

这种做法将O(N)条回执记录压缩为O(1)条记录(每条消息只需一条计数记录),极大减少了存储量。代价是无法获取完整的历史已读明细。

2.2 按时间窗口聚合

对于非实时的消息推送(例如运营类通知),业务上往往只关心“用户是否曾经阅读过该推送”或“用户是否在活动期间阅读过”。此时可以将多条推送的回执进行位图编码:

  • 为每个用户维护一个位图(Bitmap),每一位代表一条推送消息的已读状态。

  • 位图可以按时间分片(例如每月一个位图,每位对应一天的一条消息)。

  • 存储在支持位图操作的数据库或专用存储引擎中。

这种聚合方式能够将大量回执记录压缩到极小的存储空间内,但需要消息ID与位偏移之间建立固定的映射关系,适用于消息总量可预期且用户独立的场景。

3. 变更存储引擎:从关系型数据库到LSM存储

传统关系型数据库(如MySQL的InnoDB)在处理大规模写入删除时会产生较高的写放大和碎片。而已读回执场景恰好是“写多读多,且顺序写入为主”。此时可以改用基于LSM树(Log-Structured Merge-Tree)的存储引擎,如某些分布式NoSQL数据库。

LSM数据库的优势在于:

  • 写入性能极高,因为所有写入先写到内存结构再批量刷盘。

  • 数据自动压缩,尤其适合存储大量小记录。

  • 内置的时间分区(TTL)功能:可以给每行数据设置生存时间,到期自动物理删除,无需执行复杂的DELETE语句。

例如,可以为已读回执表设置TTL=90天。90天前的数据将由存储引擎在后台自动回收,对在线业务无影响。这相当于以极低的运维成本实现了数据自动淘汰。

4. 写入时去重与更新语义优化

已读回执的一个典型特点是:用户对一条消息最多只会从“未读”变为“已读”,且状态变化仅发生一次。在数据库设计中,应避免产生多条重复记录或冗余的更新历史。

  • 使用INSERT ON DUPLICATE KEY UPDATE 或 Upsert 语义:如果回执表以(消息ID,接收方ID)作为联合主键或唯一索引,则每个接收方对每条消息在表中最多存在一行。用户的首次已读行为插入记录,后续重复上报只更新时间戳(如有需要)但不再新增行。

  • 避免写审计日志:不要额外记录状态变更历史,除非明确有合规需求。

很多系统因为代码实现不当(如先查询后判断再插入)导致在高并发下产生重复记录,或者因为消息重发机制导致同一条已读事件被多次写入。因此必须确保写入操作是幂等的。

5. 降级回执精度:从精确明细到抽样或摘要

在某些业务场景下,用户并不需要100%精确的已读明细。例如,管理后台显示“整体已读率80%”就足够了,无需知道具体每个人每一条消息的状态。此时可以采用采样或摘要的方式:

  • 采样回执:只记录一定比例用户(例如10%)的已读状态,用统计结果推断整体。

  • 用户级摘要:只记录每个用户最后一次已读时间、或对某类消息的最后已读消息ID。

  • 消息级摘要:记录每条消息的已读总数和最近几个已读用户的ID(用于展示),放弃全部明细。

这种降级虽然损失了数据的完整性,但在存储成本极高或业务价值有限的场景下是最理性的选择。实际上,很多社交产品对较早的历史消息也不再提供“哪些人已读”的明细查询,正是此原因。

6. 合理使用缓存层卸载读压力

已读回执的查询模式往往具有局部性:用户刚发送消息后,短时间内会频繁查看哪些人已读;而几天后几乎不再访问。因此,可以设计双层存储:

  • 近期活跃消息的热回执:存放在Redis等KV缓存中,数据结构可以是Hash或Set。例如 key = read_receipt:{msgId},value为已读用户ID集合或位图。缓存中的回执在消息发送后的前N天保持有效。

  • 缓存淘汰后:将冷门的回执数据写入廉价持久存储,但写入时已进行过聚合(如只保留计数)。

当查询请求到来时,先查缓存,缓存未命中再查后端持久存储。由于绝大部分查询都集中在近期的热点消息上,缓存命中率高,持久存储的访问频率降低,从而可以接受较为厚重的压缩或归档存储。

7. 分区与分片调优

如果最终仍然需要保留明细数据,那么必须做好物理分区。分区键的选择至关重要:

  • 按接收方ID哈希分片:将同一个用户的所有已读回执分布到固定分片,便于查询“某个用户的所有已读消息”的场景。

  • 按消息创建时间范围分区:便于批量删除或归档旧分区。

可以组合使用:先按时间分区(例如每天一个分区),再在每个分区内按接收方ID分片。这样写操作只写入当天分区,且分布均匀;查询时若指定消息时间范围,可快速定位分区。

三、不同业务场景下的策略选择

没有一种策略适用于所有系统。需要根据业务特点进行组合:

  • 私聊消息:每条消息只有一个接收方,回执记录数等于消息数。如果不存储明细,则无法知道用户是否已读。但可以通过“最后阅读时间”结合消息时间推断,不需要单独为每条消息存一行。可采用“每个会话最后已读位置”的方式。

  • 中小规模群聊(<500人):可暂存明细,配合TTL自动清理(如保留90天),超过时限后只保留聚合计数。因为群人数少,总量可控。

  • 大型广播或运营推送(>10万人):绝对不能存储每条推送的每个用户回执明细,必须采用聚合计数、采样或位图方式。

  • 合规审计场景(如金融、司法):可能需要永久保留所有已读回执的完整记录。此时只能通过归档到廉价对象存储并结合列式存储(如Parquet)来降低成本,而不能直接暴增在线数据库。

四、总结

消息推送的已读回执功能导致数据库暴增,本质上是因为将高维的(消息×用户)状态做了全量持久化。解决思路围绕两条主线展开:减少存储量控制生命周期

减少存储量的手段包括:按会话聚合、按时间窗口聚合、位图编码、采样降级、以及从行存储转向LSM引擎的自动压缩;控制生命周期的手段包括:冷热分离、TTL自动过期、分区批量删除、以及向对象存储归档。

在工程实践中,推荐采用“热数据保留明细+缓存加速,温数据保留计数,冷数据归档或丢弃”的分层架构。每一层选择最适合的存储技术:近期明细存在高性能NoSQL或关系库,中期统计存到计数表,远期数据落到对象存储且不提供在线随机查询。

最终,系统设计者需要接受一个事实:已读回执的精确性与存储成本之间存在权衡。在没有无限存储预算的前提下,必须明确业务对回执数据“保留多久、精确到何种程度、是否支持全量查询”的真实需求,从而选择上述策略的恰当组合。只有通过主动设计与取舍,才能从根本上避免数据库的无序膨胀,保证消息推送系统的稳定与可持续性。

关键词:
分享到: