什么是 Feeds 流? 从用户层面来说, 各种手机 APP 里面, 特别是社交类的, 我们可以看到关注的内容、好友的动态聚合成一个列表(最典型的就是微信朋友圈)都是 feeds 流的一种形式。
Feeds 流的核心功能就是: 信息聚合 它可以根据你的行为去聚合你想要的信息,然后再将它们以轻松易得的方式提供给你。这个方式就是信息流的方式,你只需要不断的滑动,就可以再各种信息中穿梭,而不需要自己去寻找,被动接收信息。 例如:微博是通过你的关注列表了解你可能想要的信息源,而后以时间轴的形式聚合各种信息推给你。后来又出现了抖音的猜你喜欢,它不需要你的手动关注,而是根据你的阅览时长,点赞等信息生成你的用户画像,从而聚合你可能感兴趣的信息。朋友圈的Feeds流则是根据你的好友关系,从而聚合了你可能想要的信息。
从信息源聚合来看, Feeds 的信息源聚合有三种场景:
从展示逻辑上来看, 又分为两种:
名称 | 说明 | 备注 |
---|---|---|
Feed | Feed流中的每一条状态或者消息都是Feed,比如朋友圈中的一个状态就是一个Feed,微博中的一条微博就是一个Feed | 无 |
Feeds流 | Feed流本质上是数据流,核心逻辑是服务端系统将 “多个发布者的信息内容” 通过 “关注收藏屏蔽等关系” 推送给 “多个接收者”.如公众号订阅消息 | 三大特点:少部分人发布;基于订阅行为关联关系;大多数人读取信息 |
Timeline | Timeline其实是一种Feed流的类型,微博,朋友圈都是Timeline类型的Feed流,但是由于Timeline类型出现最早,使用最广泛,最为人熟知,有时候也用Timeline来表示Feed流 | 又称为时间轴 |
关注页Timeline | 展示其他人Feed消息的页面,比如朋友圈,微博的首页等。 | 又叫做收件箱,每个用户能看到的消息都会被存储到收件箱中 |
个人页Timeline | 展示自己发送过的Feed消息的页面,比如微信中的相册,微博的个人页等 | 又叫做发件箱,自己发布的消息都会被记录到自己的发件箱中。别人的收件箱内的消息,也是从他的各个关注人的发件箱内同步过来的 |
timeline feeds 是根据用户之间的关系来召回 Feed, 然后基于发布时间排序的 feeds 流系统。
一个 timeline Feeds 模型需要开发的功能包括:
feeds 流系统通用的特点(挑战):
根据上述需要设计的功能, 以及通用问题, 进一步对问题进行抽象分析, 并给出解决方案
在比较早之前,由于某个明星公布了一个私人消息导致微博访问量飙升直到系统崩溃, 微博做出了一系列扩容调整后宣布系统的吞吐量能支撑多位明星 “并发出轨” 实际上在数据密集系统设计(DDIA) 中提出过 ladygaga 问题:
这里引申出两种方案: 读扩散和写扩散 问题
读扩散实现:
读扩散分页问题: 由于读扩散下,用户的收件箱是实时计算出来的,翻页的时候,需要去所有关注人的发件箱中拉取一定量的数据。拉取后,需要记录当前拉取到了写信箱的 write_last_id,多少个关注就要记录了多少个 write_last_id。而后翻页的时候,需要用这些write_last_id往后拉取新的一定量(比如page_size个)的数据。再用这些数据组成的新收件箱列表,筛选 page_size 条返回前端。同时,还需要更新他实际拉取了消息的写信箱中的 write_last_id,并且存储。当下一次翻页的时候,这批 write_last_id 将作为下次的翻页时定位的依据
总结: 读扩散模式,写 feed 逻辑简单, 节约存储, 但是读性能差, 分页功能实现复杂
写扩散实现:
写扩散下分页: 由于用户收件箱都是写好的, 直接用 last_id 往下翻即可
总结: 写扩散模式读性能较好,但是浪费存储, 并且大V用户写扩散太慢会出现时效性问题
所谓遇事不决,推拉结合。 我们上面提到过 feeds 流系统是一个读多写少的系统, 所以选择写扩散会更好, 不过针对上面提到的大V用户问题对写的放大太严重了, 性能受到较大影响。
所以我们采取推拉结合模式:
具体操作:
当我们解决了大V的写扩散问题后, 又面临着新的问题:
针对上面的问题, 我们需要有一套体系对用户进行分级, 如何标识是大V ,如何标识是活跃用户
针对大V用户进行打标:
针对活跃用户进行用户分级:
如果用户关注的列表过多,会导致这个用户的收件箱列表成为一个大 key, 这类用户的性能上会有影响
写扩散模式下,用户发布消息可以慢慢扩散出去,但是删除,修改都要扩散出去,速度过慢会出现时效性问题。而且,如果真的是删除了数据,可能会影响Feeds流的分页功能)
这种情况, 我们可以采用软删除+懒删除机制: 软删除是指消息内容不进行实际删除,而是将消息置为删除状态即可,不扩散出去。如此一来,用户在自己的读取收件箱中消息的时候,是先获取了消息 Id 后,再去数据库查出消息内容,而后判断状态进行过滤,把已经删除的状态剔除,不返回给前端。此时也需要重新进行捞数据,填充分页内容。 懒删除是指如果过滤了某个消息,此时才把消息从用户收件箱中真正删除。(redis的zset中的对应id进行剔除,完成Feeds流表的刷新)
软删除+懒删除的机制具体的实现方案较: 读扩散回查: 我们在写扩散时,只写了一个消息id到用户的收件箱中,所以,用户查询收件箱信息的时候,要进行一个回查将信息丰富(该方案相比直接把内容一起写入收件箱内会更加节约内存,减少冗余数据,同时消息删除无需扩散)。
整体架构设计如下:
feed 的核心逻辑主要是发布消息+拉取 feeds 流, 核心底层存储为一个关系型数据表存储消息原始内容, 两个 redis list 对应收发件箱
消息表的存储结构设计如下:
字段名称 | 字段说明 | 备注 |
---|---|---|
msg_id | 消息唯一标识 | |
msg_title | 消息标题 | |
msg_content | 消息内容 | json 存储 |
msg_type | 消息类型 | 如文字、视频等 |
msg_status | 消息状态 | 用于状态标识, 如审核、软删除等 |
extra_info | 扩展信息 | 用于业务扩展需求, 存储 json |
sender_id | 发送人 | |
create_at | 发送时间 | |
modify_at | 修改时间 |
收/发件箱使用 redis zset 存储, 以收件箱为例: key是 接收者uid
,zvalue为发件人uid+消息id
,zscore:发布时间戳
。这样设计,可以将计算下沉,每次收件箱出现消息的刷新的时候,都会自行排序。
1 | key: 接收者uid -> value: 发件人uid+msg_id -> scroe: 消息发布时间戳 |
---|
发布一条Feed消息的时候,流程是这样的:
用户刷新自己的Feed流程是这样的: