见山之后 Beyond the Mountain
实战教学 /

深入解析消息中心架构:推拉模型、读写扩散与存储选型实战

在当今的内容型产品中,无论提供何种类型的内容,除了核心功能外,消息中心往往是一个不可或缺的重要模块。

无论是信息流、论坛、信箱,还是私聊、群聊或系统通知,“推拉模型”都是内容型(含社交型)产品架构的核心。做出正确选择的关键,在于对产品形态和系统组件的清晰认知。

本文将聚焦于消息中心,深入探讨其设计思路。

需求分析

消息中心通常包含以下两类功能(如下图所示):

  1. 用户通知:如点赞、评论、关注、@ 等互动消息。
  2. 官方通知:由系统或运营发布的公告类消息。

接下来,我们对这两类通知进行抽象分析。

首先,用户通知具有高度的个性化特征(例如,你的点赞列表与我的截然不同)。因此,我们需要为每个用户维护一个独立的 「收件箱」

当用户 A 点赞了用户 B 的内容时,后端系统在接收到该事件后,会将点赞信息写入 B 的 「收件箱」,并记录“A 在 xxx 时间点赞了 xxx 内容”。这是一个系统将消息主动 推送 给接收者的过程。

而对于 官方通知,其内容对绝大多数用户是统一的(尽管可能存在屏蔽设置或定向发送)。由于官方通知由系统统一下发,系统只需维护一个全局的 「发件箱」

「发件箱」存储了所有待分发的官方通知。当用户打开消息中心时,会主动向系统 「拉取」 最新的官方消息,并与自己「收件箱」中的记录比对,以确认已读状态。这是一个用户主动从系统 「拉取」 通知的过程。

推拉模型

上述分析揭示了两种场景背后的核心机制——推拉模型。选择不同运行机制的根本原因,在于解决 读写扩散 问题。

推模型

先看推模型。对于内容创作者而言,最愉悦的体验莫过于打开应用时看到大量的点赞或评论提示。对于大 V 来说,他人点赞的频率远高于其查看消息的频率,这是典型的 “写多读少” 场景。每当有用户点赞,系统都会将索引信息(如内容 ID、类型、发布时间等)写入该大 V 的收件箱中。

  • 优点:读取轻量。仅需读取本地消息列表即可。
  • 缺点:写入沉重。若用户内容热度极高,瞬间涌入的大量点赞或评论会导致密集的写入操作。

拉模型

再看拉模型。以官方通知为例,通常由运营人员发布,频率较低(可能每月仅几条),但用户每次进入 App 时都会检查是否有新通知,这是典型的 “读多写少” 场景。

  • 优点:写入轻量,节省存储空间。系统只需维护一份全局消息列表。
  • 缺点:读取沉重,计算量大。若存在多个官方通知来源(如淘宝内的各类官方业务),每次拉取都需要从多个生产者处获取最新内容并进行聚合。

流程设计

用户通知

用户通知的流程设计如下:

在该流程中,需注意以下关键点:

异步发送

当用户触发点赞、关注或评论行为时,被互动方无需立即感知,因此也不必同步将互动信息写入其收件箱。建议采用 消息队列 进行异步通知,以缓解系统压力。

缓存前置

若直接将消息写入数据库中的用户收件箱,可能导致用户在请求消息列表时将大量流量直接打到数据库,引发系统故障。因此,通常在更新用户收件箱时,采用 双写策略,同时更新用户缓存。

官方通知

相较于用户通知,官方通知引入了“运营人员”这一角色,操作流程更为复杂,系统设计也随之增加了复杂度。

官方运营将通知发送至 「发件箱」,其中保留所有在线通知列表。用户查看通知时,需从 官方「发件箱」 获取未读通知,并从 个人「收件箱」 查询历史通知。具体流程为:

  1. 运营写入发件箱
  2. 用户读取发件箱
  3. 用户写入收件箱(标记已读或归档)

流程示意图如下:

  1. 官方运营在后台编辑并发布通知,数据持久化存储至数据库(此处选用 MySQL,后续章节将详述)。
  2. 通知发生变更时,发送变更消息。基于该消息更新单条通知的缓存,并刷新官方发件箱列表以供前台查询。
  3. 用户查看通知列表时,若是第一页,需先检查官方发件箱队列中是否有未读通知。
  4. 若存在未读通知,将其与历史通知的第一页合并后返回给用户,并异步将该通知写入用户的收件箱中。

持久化方案

理清核心业务流程后,接下来的关键问题是:数据存储在哪里?

前文提到,官方通知的「发件箱」使用 MySQL 进行持久化。这是因为官方通知数量较少,且属于拉模型,具备 “重读轻写” 的特征,大部分压力由缓存承担,因此底层使用 MySQL 存储并无瓶颈。

难点主要在于用户的 「收件箱」

如前所述,用户收件箱采用推模型,属于 “重写轻读” 场景。一旦大 V 发布热门内容,其收件箱可能在瞬间承受巨大的写入流量。对于头部大 V 而言,累积数千万点赞并非难事,每条点赞信息均需写入其收件箱,这就要求底层存储必须支持海量数据的高并发写入。

在此场景下,MySQL 可能不再是最佳选择。我们可以尝试使用 HBase

MySQL 与 HBase

MySQL 和 HBase 是我们日常应用中常用的两个数据库,分别解决应用的在线事务问题和大数据场景的海量存储问题。 综合对比

MySQL:是常用的数据库,采用行存储模式,底层是 binlog,用来存储业务数据,数据存储量较小。

HBase:列式数据库,底层是 hdfs,可以存储海量的数据,主要用来存储海量的业务数据和日志数据。

从引擎结构看差异

HBase 和 MySQL 的核心差异在于底层的数据结构,HBase 使用 LSM(Log-Structure Merge)树,Innodb 使用 B+树。

LSM 树,即日志结构合并树(Log-Structured Merge-Tree)。 其实它并不属于一个具体的数据结构,它更多是一种数据结构的设计思想。

它的核心思路其实非常简单,就是假定内存足够大,因此不需要每次有数据更新就必须将数据写入到磁盘中,而可以先将最新的数据驻留在内存中,等到积累到最后多之后,再使用归并排序的方式将内存内的数据合并追加到磁盘队尾 (因为所有待排序的树都是有序的,可以通过合并排序的方式快速合并到一起)。

LSM 具有批量特性,存储延迟。当写读比例很大的时候(写比读多),LSM 树相比于 B 树有更好的性能。因为随着 insert 操作,为了维护 B 树结构,节点分裂。 读磁盘的随机读写概率会变大,性能会逐渐减弱。 多次单页随机写,变成一次多页随机写,复用了磁盘寻道时间,极大提升效率。

因此,由引擎结构(B+Tree vs LSM Tree)看到的能力差异:

  1. MySQL:读写均衡、存在空间碎片

  2. HBase:侧重于写、存储紧凑无浪费、Io 放大、数据导入能力强

从架构对比看差异

相比 MySQL,HBase 的架构特点:

  1. 完全分布式(数据分片、故障自恢复)
  2. 底层使用 HDFS(存储计算分离)。

由架构看到的能力差异:

  1. MySQL:运维简单(组件少)、延时低(访问路径短)
  2. HBase:扩展性好、内置容错恢复与数据冗余

总结

本文我们讲述了如何从官方通知和用户通知两个方面切入,设计一个 App 的常见功能——消息中心。但该方案仍然有很多潜在的问题:如果官方通知的来源很多呢?如何解决写扩散带来的成本问题?这些都是值得探索的问题。

事实上,消息中心虽然是一个十分常见的功能,但背后涉及到的东西非常复杂,发布/订阅、推拉模型、读写扩散等问题都会影响到我们的架构设计。

架构设计的过程,就是取舍的过程,而如何取舍,则是一门学问。对于现在纷繁复杂的互联网业务,永远没有最好的架构,只有最适合的架构。

最后,我们抛个问题,朋友圈是推模型还是拉模型?