Timeline Feed服务
内容总结自《亿级流量系统架构设计与实战》
一、概述
1、分类
Feed 流的功能在当今的互联网应用和网络社交平台中非常重要,它是一种以时间线为基础的信息流展示形式,把用户感兴趣的内容呈现在用户的Feed页面上。如果你使用过一些互联网应用就会发现,很多互联网应用的主页都是Feed页面,它们把Feed流当作自己的“门面”。Feed 流在内容聚合维度上包括但不限于如下几种形式。
- 推荐 Feed 流:按照你的浏览兴趣菜合内容,你可能不认识 Feed 流内容的发布者,但是他发布的内容你可能很感兴趣。
- 关注 Feed 流:你关注的用户发布的内容被聚合为 Feed 流,并且按照内容的发布时间从近到远展示你所关注的那些人最近发布的内容。按照内容的发布时间排序,也就是遊循时间线,所以这种 Feed 流是一种 Timeline Feed 流。徽信朋友閣和微博首页都是典型的对关注 Feed 流的应用。
- 附近 Feed 流:顾名思义,就是你附近的用户最近发布的内容,这对于社交类应用来说较为常见。推荐 Feed 流的重点是推荐算法,附近 Feed 流的重点是地理位置判断,其相关技术差异巨大,不具备通用性
2、功能
Timeline Feed 流提供的数据应该是我们所关注的人在指定的时问段内发布的内容列表,并且内容按照时间由近及远排序。
用户在客户端浏览Timeline Feed页面时一般有如下两种操作方式。
- 下拉操作:刷新Feed流,拉取当前时间最新的 N条 Feed 流。
- 上滑操作:拉取更早时间的 N条Feed 流。
总之,Timeline Feed 服务主要负资两种读取数据的方式:下拉与上滑。
其中,下拉负责拉取用户从未看过的最新内容列表,上滑负资拉取更早的内容列表,并限制所能拉取到内容最大数量。无论是何种读取方式,内容列表均找照内容的发布时间从近到远排序。
二、设计原理
1、拉模式与用户发件箱
较为符合我们直觉的实现Timeline Feed服务的方式是拉模式。每个内容发布者都有自己的“发件箱”,每当用户发布一个内容时,就把内容存放到发件箱中,由其他用户来拉取内容
在拉模式下,用户每刷新一次Feed流,系统就需要读取N个用户的发件箱(这里的N指用户关注的人数),这意味着一次用户请求会放大产生N倍的读请求,故而这种模式也被称为“读扩散”。如果用户量级较大,那么获取Feed流会是一个高并发场景,而且用户关注的人数也会较多。所以这种扩散读会增加用户请求延迟,并可能击垮存储用户内容列表的服务器
2、推模式与用户收件箱
与拉模式相反,在推模式下,每个用户都有一个“收件箱”,当某个用户成功发布了内容时,系统会将该内容推送到其每个粉丝用户的收件箱中。粉丝用户在获取Feed流时,直接从收件箱中读取内容即可。在推模式下,用户获取Feed流的性能比在拉模式下好。
缺点:
- 存储压力大:每个用户都有收件箱,势必增加收件箱存储开销,用户越多,收收件箱占用的存储资源也越多
- 写扩散:某用户存在100w粉丝,当发布完内容后,需要推送到100w粉丝的收件箱,即会产生100w个写请求。巨量写请求,会击垮收件箱所依赖的数据库
3、推拉模式结合
推模式和拉模式优缺点互补,可以相互结合。大V的内容就拉模式(推模式要推的人太多),普通内容推模式(拉模式牵涉的人数太多)
用户-区分活跃用户
大V在发布内容后,我们依然可以采用推模式,但是现在仅将内容推送给粉丝列表中的部分活跃用户,因为这些用户使用 Timeline Feed 流功能的频常相对较高,所以将内容主动推送到他们的收件箱中更有可能提高获取Timeline Feed流的性能。至于那些很长时间都没有打开应用的用户,则完全没有必要把内容存储到他们的收件箱中。如果有一天这些用户登录应用并使用Timeline Feed流功能,那么保持采用推拉结合模式结合来获取数据就好。
综上所述,对推拉模式的结合方式总结如下:
- 如果内容的发布者是普通用户,则完全可以采用推模式,把内容推送到全部粉丝的收件箱中
- 如果内容的发布者是大V,则进一步区分活跃用户和非活跃用户。对于活跃用户,采用推模式;对于非活跃用户,则采用拉模式
三、关键技术
1、内容与用户收件箱的交互(推模式)
内容发布服务需要与Timeline Feed服务解耦,而且要尽可能提高推送的可用性,最好的办法就是在这两个服务之间建立消息队列通道。我们可以创建一个消费者服务负责接收内容发布服务的内容变更事件,如果发现有新内容发布,则执行遍历推送操作
2、推送拆分子任务
Timeline消费者遍历推送毕竟是串行操作,如果需要将一条内容推送给更多的粉丝,那么遍历推送可能会消耗更长的时间,进而造成内容发布服务与Timeline消费者之间的消息队列中的消息积压,导致内容到用户收件箱的投递延迟。我们可以将对大量粉丝的遍历推送拆分为多个并行执行的子任务,每个子任务负责对一批粉丝的推送。
比如需要将内容A推送给3000个粉丝,且粉丝的用户ID是1~3000,那么可以将这个任务拆分为3个子任务
- 子任务1:负责将内容发送给粉丝1~1000
- 子任务2:负责将内容发送给粉丝1001~2000
- 子任务3:负责将内容发送给粉丝2001~3000
3、收件箱模型设计
用户查询时的业务动作含义是:以某个时间点为基准,向前或者向后获取当前用户关联的内容id
1)使用数据库
新增表:inbox
字段名 | 类型 | 含义 |
---|---|---|
id | bigint | 主键 |
user_id | bigint | 用户id |
content_id | bigint | 内容id |
publish_time | datetime | 内容发布时间 |
idx_feed(user_id,publish_time,content_id)
每个用户的收件箱内容都会按照publish_time从小到大排列,当publish_time相同时,再进一步按照content_id从小到大排列,所以从后向前扫描索引正好与Timeline Feed流内容的排序规则相吻合
2)使用ZSET
- key为inbox_{用户ID},表示一个ZSET对象是哪个用户的收件箱
- Member为内容id
- Score为内容发布时间。ZSET可以按照内容发布时间从小到大排列内容ID
不断获取内容ID的方案:
- 根据last_content_id 直接定位下一条内容,但这种方案不适合推拉结合模式;
- 先获取发布时间小于或等于ts的全部内容,再过滤筛选;
- 对第二种方案的优化,把内容ID格式化为 20 位长度的字符串用作Member 字段,保证在ZSET中发布时间相同的内容按照内容ID的数值从小到大排列,这样一来,在ZSET中从后向前扫描就与Timeline Feed流内容的排序规则相吻合了。