4 从小白到架构师: Feed 流系统实战( 三 )


缓存不足是计算机领域的经典问题了,问问你的 CPU 它就会告诉你答案 —— 一级缓存不够用就做二级缓存,L1、L2、L3 都用光了我才会用内存 。
只要是支持有序结构的 NewSQL 数据库比如 Cassandra、HBase 都可以胜任 Redis 的二级缓存:

4 从小白到架构师: Feed 流系统实战

文章插图
附上一条 Cassandra 的表结构描述:
-- Cassandra 是一个 Map<PartionKey, SortedMap<ClusteringKey, OtherColumns>> 结构-- 必须指定一个 PartionKey,顺序也只能按照 ClusteringKey 的顺序排列-- 这里 PartionKey 是 uid, ClusteringKey 是 publish_time + article_id-- publish_time 必须写在 ClusteringKey 第一列才能按照它进行排序-- article_id 也写进 ClusteringKey 是为了防止 publish_time 出现重复CREATE TABLE taojin.following_timelins (uid bigint,publish_time timestamp,article_id bigin,PRIMARY KEY (uid, publish_time, article_id)) WITH default_time_to_live = 60 * 24 * 60 * 60;这里还是要提醒一下,每多一层缓存便要多考虑一层一致性问题,到底要不要做多级缓存需要仔细权衡 。
还有一些细节要优化分页器Feed 流是一个动态的列表,列表内容会随着时间不断变化 。传统的 limit + offset 分页器会有一些问题:
4 从小白到架构师: Feed 流系统实战

文章插图
在 T1 时刻读取了第一页,T2时刻有人新发表了 article 11 ,如果这时来拉取第二页,会导致 article 6 在第一页和第二页都被返回了 。
解决这个问题的方法是根据上一页最后一条 Feed 的 ID 来拉取下一页:
4 从小白到架构师: Feed 流系统实战

文章插图
使用 Feed ID 来分页需要先根据 ID 查找 Feed , 然后再根据 Feed 的发布时间读取下一页,流程比较麻烦 。若作为分页游标的 Feed 被删除了,就更麻烦了 。
笔者更倾向于使用时间戳来作为游标:
4 从小白到架构师: Feed 流系统实战

文章插图
使用时间戳不可避免的会出现两条 Feed 时间戳相同的问题, 这会让我们的分页器不知所措 。
4 从小白到架构师: Feed 流系统实战

文章插图
这里有个小技巧是将 Feed id 作为 score 的小数部分 , 比如 article 11 在 2022-10-27 13:55:11 发布(时间戳 1666850112),那么它的 score 为 1666850112.11 小数部分既不影响按时间排序又避免了重复 。
4 从小白到架构师: Feed 流系统实战

文章插图
大规模推送虽然我们已经将推送 Feed 的任务转移给了 MQ Worker 来处理,但面对将 Feed 推送给上百万粉丝这样庞大的任务, 单机的 Worker 还是很难处理 。而且一旦处理中途崩溃就需要全部重新开始 。
我们可以将大型推送任务拆分成多个子任务,通过消息队列发送到多台 MQ Worker 上进行处理 。
4 从小白到架构师: Feed 流系统实战

文章插图
因为负责拆分任务的 Dispatcher 只需要扫描粉丝列表负担和故障概率大大减轻 。若某个推送子任务失败 MQ 会自动进行重试,也无需我们担心 。
总结至此,我们完成了一个关注 Feed 流系统的设计 。总结一下本文我们都讨论了哪些内容:
  • 基本模型有两种 。推模型:发布新 Feed 时推送到每个粉丝的 Timeline; 拉模型:打开 Timeline 时拉取所有关注的人发布的 Feed,重新聚合成粉丝的 Timeline 。推模型读取快,但是推送慢 , 粉丝数多的时候峰值负载很重 。拉模型没有峰值问题,但是读取很慢用户打开 Timeline 时要等待很久,读极多写极少的环境中消耗的计算资源更多 。
  • 头部用户的几十上百万粉丝中活跃用户比例很少 , 所以我们可以只将他们的新 Feed 推送给活跃用户,不活跃用户等回归时再使用拉模型重建 Timeline.即通过「在线推、离线拉」的模式解决推模型的峰值问题 。
  • 虽然关注 Timeline 数据很多但实际上是一种缓存 , 没必要全部存储 。我们按照缓存的思路只存储活跃用户、最近一段时间的数据即可,没有缓存的数据在用户阅读时再通过拉模型重建 。
  • Timeline 推荐使用 Redis 的 SortedSet 结构存储,Member 为 FeedID,Score 为时间戳 。给缓存设置自动过期时间,不活跃用户的缓存会自动被清除 。使用「在线推 , 离线拉」时只给 Timeline 缓存未失效的用户推送即可
  • 在 Redis 内存不足时可以使用 Cassandra 作为 Redis 的二级缓存 。

推荐阅读