Feed 流系统杂谈
请移步重制版: 从小白到架构师(4): Feed 流系统实战#
什么是 Feed 流#
Feed 流是社交和资讯类应用中常见的一种形态, 比如微博知乎的关注页、微信的订阅号和朋友圈等。Feed 流源于 RSS 订阅, 用户将自己感兴趣的网站的 RSS 地址登记到 RSS 阅读器中, 在阅读器里聚合成的列表就是 Feed 流。
Feed 流的本质是 M 个用户订阅了 N 个信息源形成的多对多关系,Feed 流系统需要聚合用户订阅的 N 个信息源产生的信息单元(Feed),并按照一定顺序排列后推送给用户。
接下来我们以关注页场景为例来介绍 Feed 流的实现。
Feed 流有两种基本实现模式:
- 推模式:当新的 Feed 发布后,将这条内容插入到发布者所有粉丝的 Feed 流中。
- 拉模式:收到用户拉取Feed流请求后遍历他的关注关系,并拉取关注的人发布的内容实时聚合成 Feed 流。
两种实现方式各有优缺:
- 推模式的优点在于可以迅速响应用户拉取 Feed 流的请求。但是在粉丝数较多的大V发布内容时需要在他每个粉丝的Feed流中一一进行插入,会产生较大的峰值负载。由于 Feed 发布后的插入操作较多, 通常需要使用 MQ 来异步地进行。
- 拉模式的优点在于大V发布内容时不会产生峰值负载,但是实时构建 Feed 流操作需要用户等待较多时间。
在实际应用中我们通常采取推拉结合的实现方式。在具体介绍推拉结合之前,我们先来讨论一下 Feed 流系统的存储。
Feed 流的存储#
Feed 流系统中需要存储的数据有 3 部分:
- 作者发布的 Feed 列表:这些数据需要可靠的持久化存储,通常采用 MySQL 等关系型数据库即可。因为很可能需要按照发布时间排序, 若要使用 NoSQL 最好使用支持有序存储的数据库。
- 用户和作者之间的关注关系:同样需要可靠的持久化存储,采用 MySQL 等关系型数据库或者 KV 结构的 NoSQL 数据库均可。
- 用户的 Feed 流:Feed 流可以根据 Feed 数据库和关注关系构建,因此可以不做持久化存储。
最轻量的解决方案是使用 Redis 存储 Feed 流。在数据量较大 Redis 内存不够用时,也可以采用一些持久化的存储方案。
Redis 的 SortedSet 是非常适合存储 Feed 流的数据结构。一般以 Feed 的 ID 作为 SortedSet 的 member,时间戳或者热度值、推荐值作为 score 进行排序。SortedSet 保证了 Feed 不会重复,且插入过程线程安全,无论是推拉模式实现起来都非常方便。
为了避免 Redis 中缓存的 Feed 流占用过多内存,通常需要给 Feed 流设置 TTL.
Feed 的具体内容存储可以在 MySQL 中,同时在 Redis 中做一层缓存。关注关系可以存储在 MySQL 中,因为有些大V的粉丝数较多所以不推荐用 Redis 缓存。
持久化存储#
一个用户的 Feed 流大小是他所有关注者发布的 Feed 数总和。在用户量较大的系统中 Feed 流数据量巨大且增长迅速,将所有 Feed 流存储在 Redis 中需要消耗巨量的内存。
在必要的时候可以利用持久化存储作多级缓存,比如:将当日活跃用户的 Feed 流数据存储在 Redis 中, 当月活跃用户的 Feed 流持久化到数据库中,长期未活跃的用户则在他重新登录后使用 MySQL 中存储的关注关系重新构建 Feed 流。
因为持久化存储 Feed 流的数据库需要有较大的数据容量、较高吞吐量并且需要支持排序,所以不建议使用数据容量较小的 MySQL 或者不支持排序的 KV 数据库来存储 Feed 流数据。
作者推荐使用 Cassandra 来持久化存储 Feed 流:使用用户的 UID 作为 Partition Key, Feed 时间戳在前 Feed ID 在后, 共同作为 Clustering Key 用于排序和去重。Cassandra 支持 TTL 可以用来自动清除冷数据。
另外,由于Feed 流数据属于只追加不修改与 Cassandra 使用的 LSM 结构非常契合,可以有效减少 Cassandra 进行 Compaction 的负担。
Feed 流系统优化#
在线推 离线拉#
一个拥有 10 万粉丝的大V在发布微博时,他的粉丝中可能只有 1 千人在线。因此我们常用的优化策略是:对于在线的粉丝采用推模式,将新的 Feed 直接插入到粉丝的信息流中;对于离线的粉丝采用拉模式,在粉丝登录时遍历他的关注关系重新构建 Feed 流。
在线推的部分需要计算粉丝和在线用户的交集,然后进行插入操作。因为在线用户数和粉丝数都比较大,所以计算交集的过程需要分批进行。比如说每次查询 100 个粉丝,然后去查询这 100 个用户中有多少在线(取交集),直到遍历完粉丝列表。这个过程类似于将两个表做 join, 同样适用小表驱动大表
的原则以减少取交集操作的次数, 大多数情况下使用数量较少的粉丝表作为驱动表。(不要问我什么情况下用在线用户表做驱动表🙂)
分页器#
由于 Feed 流通常比较大,不可能一次性将所有内容拉取到本地,所以一般需要支持分页查询。
若在用户浏览过程中他关注的人发布了新的内容,导致原来在第 1 页最后一位的 Feed A 被挤到了第 2 页首位。在使用 Limit + Offset 分页器拉取第 2 页时就会再次拉到 Feed A。于是客户端上显示了两条相同的内容,这个问题非常影响用户体验。
解决重复问题最简单的方法是使用 LastId + Limit 式的分页器。客户端加载下一页时使用本地最后一个 Feed 的 ID 作为游标,服务端使用 ZRangeByScore 命令获得发布时间比它更早的 Feed 作为下一页。无论浏览过程中 Feed 流内被插入了多少新内容,只要 Feed 的时间戳唯一就不会下发重复的 Feed.
一个简单实用的避免时间戳重复的方法是:以发布时间作为 score 的整数部分,Feed ID 作为小数部分。这样 Feed ID 不会干扰排序,此外 Feed ID 不会重复所以 score 也不会重复。
深度分页#
由于 Feed 流比较大而用户大多数时候只浏览最新的内容,所以通常不需要缓存全部 Feed 流只需要缓存最新的部分即可。但是我们无法阻止用户继续向下浏览未缓存的内容,所以还是得想办法支持深度分页。
我们在实践中采用的解决方案是: 默认缓存最近一个月的数据,当用户快浏览完缓存内容时则异步地采用拉模式构建最近一年的 Feed 流缓存起来。当用户快读完最近一年的内容时继续缓存更旧的 Feed 流,直至缓存了完整 Feed 流。在追加 Feed 流缓存的同时减少它的 TTL, 以避免过大的 Feed 流长期占据内存。
作者:finley
出处:https://www.cnblogs.com/Finley/p/15391173.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
2016-10-10 BP神经网络与Python实现