Feed流使用详解

Feed流概念

Feed流是一个目前非常常见的功能,在众多产品中都有展现,比如微博,朋友圈,消息广场,通知,IM等。通过Feed流可以把动态实时的传播给订阅者,是用户获取信息流的一种有效方式。

  • Feed:Feed流中的每一条状态或者消息都是Feed,比如朋友圈中的一个状态就是一个Feed,微博中的一条微博就是一个Feed。
  • Feed流:持续更新并呈现给用户内容的信息流。每个人的朋友圈,微博关注页等等都是一个Feed流。
  • Timeline:Timeline其实是一种Feed流的类型,微博,朋友圈都是Timeline类型的Feed流,但是由于Timeline类型出现最早,使用最广泛,最为人熟知,有时候也用Timeline来表示Feed流。
  • 关注页Timeline:展示其他人Feed消息的页面,比如朋友圈,微博的首页等。
  • 个人页Timeline:展示自己发送过的Feed消息的页面,比如微信中的相册,微博的个人页等。

Feed的本质就是M个用户订阅了N个信息源形成的多对多关系,Feed需要聚合用户订阅的N个信息源产生的信息单元(Feed),并且按照一定的顺序排序后推送给用户。

Feed分类

Feed的常见分类有两种:

  • TimeLine:按发布的时间顺序排序,先发布的先看到,后发布的排列在最顶端。
  • Rank:按某个非时间的因子排序,一般是按照用户的喜好度排序等,适用于推荐场景。

Feed流有两种基本实现模式:

  • 推模式:当新的 Feed 发布后,将这条内容插入到发布者所有粉丝的 Feed 流中。
  • 拉模式:收到用户拉取Feed流请求后遍历他的关注列表,并拉取关注的人发布的内容实时聚合成 Feed 流。
  • 推拉结合:也叫做读写混合,兼具推和拉两种模式的优点。

两种实现方式各有优缺:

  • 推模式的优点在于可以迅速响应用户拉取 Feed 流的请求。但是在粉丝数较多的大V发布内容时需要在他每个粉丝的Feed流中一一进行插入,会产生较大的峰值负载。由于 Feed 发布后的插入操作较多, 通常需要使用 MQ 来异步地进行。
  • 拉模式的优点在于大V发布内容时不会产生峰值负载,但是实时构建 Feed 流操作需要用户等待较多时间。

Feed流的初始化

Feed流的初始化是指当用户的Feed流还不存在的时候,为该用户创建一个属于自己的Feed流,其实就是遍历一遍关注列表,取出所有的Feed,将FeedId存放到Redis的SortedSet中,score的值如果是TimeLine类型,就取Feed创建的时间戳,如果是rank类型,就把对应权重设置进去。

Feed流的推送

Feed的推送就是更新(这里使用推拉结合模式)。更新分为两类情况:

  • 关注的用户发布、删除新的Feed
  • 用户新增、取消关注

Feed流的存储

Feed流系统中需要存储的数据有3部分:

  • 作者发布的Feed列表:这些数据需要可靠的持久化存储,通常采用MySQL等关系型数据库即可。
  • 用户和作者之间的关注关系:同样需要持久化存储。
  • 用户的Feed流:Feed流可以根据Feed数据库和关注关系构建,因此不需要持久化存储。

最轻量级的解决方式是使用Redis存储Feed流。在数据量比较大Redis内存不够用的时候,也可以采用一些持久化方案。

Redis的SortedSet是非常适合存储Feed流的数据结构,一般以Feed的ID作为Sorted的member,时间戳、热度值、推荐值作为score进行排序,SortedSet保证Feed不会重复,且插入过程线程安全,无论是推拉模式实现起来都比较方便。

为了避免Redis缓存中的Feed流占用过多内存,通常需要给Feed流设置TTL。

持久化存储

一个用户的Feed流大小是他所有关注者发布的Feed流总和,在用户量较大的系统中Feed数据量巨大而且增长迅速,将所有的Feed流存储在Redis中需要消耗巨量的内存。

在必要的时候可以利用持久化存储作多级缓存,比如:将当日活跃用户的Feed流数据存储在Redis中,当月活跃的用户的Feed流持久化到数据库中,长期未活跃的用户则在他重新登录后使用MySQL中存储的关注关系重新构建Feed流。

因为持久化存储的Feed流的数据库需要有较大的数据容量、较高的吞吐量并且需要支持排序。所以不建议使用数据容量较小的MySQL或者不支持排序的KV数据库来存储Feed流数据。

Feed流优化

(1) 推拉结合

① 在线推 离线拉

一个拥有 10 万粉丝的大V在发布微博时,他的粉丝中可能只有 1 千人在线。因此我们常用的优化策略是:对于在线的粉丝采用推模式,将新的 Feed 直接插入到粉丝的信息流中;对于离线的粉丝采用拉模式,在粉丝登录时遍历他的关注关系重新构建 Feed 流。

在线推的部分需要计算粉丝和在线用户的交集,然后进行插入操作。因为在线用户数和粉丝数都比较大,所以计算交集的过程需要分批进行。比如说每次查询 100 个粉丝,然后去查询这 100 个用户中有多少在线(取交集),直到遍历完粉丝列表。这个过程类似于将两个表做 join,同样适用小表驱动大表的原则以减少取交集操作的次数, 大多数情况下使用数量较少的粉丝表作为驱动表。

② 定时推 离线拉

一个拥有 10 万粉丝的大V在发布微博时,以常驻进程的方式定时推送到粉丝的动态列表。

(2) 分页器

由于 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 也不会重复。

(3) 深度分页

由于 Feed 流比较大而用户大多数时候只浏览最新的内容,所以通常不需要缓存全部 Feed 流只需要缓存最新的部分即可。但是我们无法阻止用户继续向下浏览未缓存的内容,所以还是得想办法支持深度分页。

我们在实践中采用的解决方案是: 默认缓存最近一个月的数据,当用户快浏览完缓存内容时则异步地采用拉模式构建最近一年的 Feed 流缓存起来。当用户快读完最近一年的内容时继续缓存更旧的 Feed 流,直至缓存了完整 Feed 流。在追加 Feed 流缓存的同时减少它的 TTL, 以避免过大的 Feed 流长期占据内存。

拓展

如何打造千万级Feed流系统-阿里云开发者社区 (aliyun.com)