分享 Feed 流设计 (二):拉模式 Vs 推模式

xiaoronglv for SAP · 2018年09月26日 · 最后由 nxy23 回复于 2018年10月12日 · 134 次阅读
本帖已被设为精华帖!

Feed 后台设计文章索引

在上一篇文章中,我们谈到「如何用事件抽象Feed中多态的内容」,这篇文章会进一步讨论动态(event)的分发问题。

当用户分享一个视频时,该视频应该出现在他 Follower 的 Feed流中。比如吕小荣和 Sam 是 facebook 好友,吕小荣更新状态「我今天感觉好极了」。Sam 登录 facebook 时,会在自己的 feed 流中看到吕小荣的状态更新。

如何实现?通常有拉和推二种做法。

方式一:拉

Sam 有7个 facebook 好友,每次 Sam 登录,只需要遍历所有好友的产生的 event,按时间逆序排列即可。

step1: 查询 Sam 朋友的主键 ID

select friend_id from friendships where user_id = 3
=> [1, 4, 8, 10, 33, 78, 101]

step2: 在上一篇文章中,我们用 event 来抽象 feed流中的事件,并且保存在 table event 中。根据好友的主键 ID 过滤 event,并且渲染给 Sam 即可。

select * from events where user_id in [1, 4, 8, 10, 33, 78, 101] order by id desc

优点

  1. 实现简单。
  2. 关注/取消关注后,feed 中的内容会动态变化,易于维护。
  3. 与「推模式」相比,没有冗余数据,不占用存储空间。

缺点

  1. 当用户超过1000个好友时,读的效率很低。
  2. 随着产品的发展,会出现很多奇奇怪怪的逻辑关系,SQL查询的效率会非常低,或者压根无法实现。
    • 假装关注。虽然我关注你了,但是我压根不想在feed中看到你/
    • 虚假的友谊。虽然我们互相关注了,但是我屏蔽了你的动态,不想在feed中看到你。

方式二:推。

  1. 每个用户拥有自己的队列,在自己的队列中读取 feed 流信息。
  2. 当关注的人产生 feed ,根据规则写到不同的队列中。写入前,检查各种规则,如果不满足条件,就不写入。

比如 Ryan 在 facebook 有三个朋友 Sam,Bell,Zoey, Bell 嫌弃 Ryan 话多,偷偷屏蔽了Ryan。

当 Ryan 发布了一条动态 「Today is great!」(红颜色6),系统应当只分发该动态至 Sam 和 Zoey 的队列。因为 Bell 屏蔽了 Ryan,所以 Ryan 的更新并不会分至 Bell 的队列。

下次 Sam/Zoey登录时,会在自己的队列中看到 Ryan 的动态更新 6;Bell登录时,则不会看到 6

优点

  1. 读的性能高。
  2. 可以覆盖各种各样的业务场景。

缺点

  1. 需要大量的写入,尤其是当某个用户有100万的 follower 时,写入会非常耗时,需要额外的优化。
  2. 当用户的关注/取消关注时,要更新队列,剔除已经失效的feed 内容。
  3. 常年不登录的僵尸用户的queue,会占用大量的存储空间,如果存储层是内存型数据库,每个月的花费不菲。所以有人会对僵尸用户采取额外的处理策略。

推模式可以灵活应对各种业务场景

随着产品的发展,会出现各种各样的商业场景和 feed 流相关。

  1. 公司拥有自己的 feed 主页。 (CompanyFeed)
    e.g. SAP 的facebook 主页

  2. 热门的 tag 会有自己的 feed 主页,用户可以关注标签。 (TagFeed)
    e.g. facebook RocksDB 的 tag页面

  3. 一本书/股票/电影/工作机会/线下聚会也会有自己的 feed 主页 (ObjectFeed)

  4. 一个人会有自己的 public feed 主页。(ProfileFeed)
    e.g. 吕小荣的facebook主页

  5. 一个兴趣小组会有自己的 feed 主页。 (GroupFeed)

  6. 一个人会有特别好友,他有时只想看特别好友的更新。 (CloseFriendFeed)

理论上讲,当 Ryan 在兴趣小组 RocksDB Developers Public 更新一条带标签的动态 "#RocksDB is great!" 时, 这条内容应该出现在 ②, ④, ⑤,⑥ 四个地方:tag RocksDB 的feed 主页;Ryan 的个人主页;兴趣小组 RocksDB Developers Public 的主页;Ryan 密友的主页。

推模式下,各个组件如何协作?

推的模式可以将分发逻辑写在代码里,而不是查询语句里。然后根据逻辑将新的动态(feed_event)分发至符合条件队列里,满足各种各样的业务需求。受众只需要在相应的队列里读取数据即可,无需复杂的查询语句,大大提高了读的效率。

下一篇文章,我们来谈如何对分发逻辑的抽象。

共收到 16 条回复

比如 Ryan 在 facebook 有三个朋友 Sam,Bell,Zoey, Bell 嫌弃 Ryan 话多,偷偷屏蔽了Ryan。

当 Ryan 发布了一条动态 「Today is great!」(红颜色6),系统应当只分发该动态至 Sam 和 Zoey 的队列。因为 Bell 屏蔽了 Ryan,所以 Ryan 的更新并不会分至 Bell 的队列。

下次 Sam/Zoey登录时,会在自己的队列中看到 Ryan 的动态更新 6;Bell登录时,则不会看到 6。

假如Bell取消了屏蔽,再次去拉Ryan的消息吗?还是一开始推到了Bell的队列,只是隐藏

假如Bell取消了屏蔽,再次去拉Ryan的消息吗?

我的考虑:Feed 的内容大部分都有时效性,过了时间之后就没有什么价值了。取消屏蔽后,是不是可以不用拉数据啊。

实际中是推拉相结合吧

我觉得拉的模式缺点讲得不正确,把event和friendship强关联的设计是这个造成这个缺点的主要原因,而不是拉模式本身的问题。

正确设计应该有一个中间表,比如 event_subscribers ,表结构是这样的:

user_id, subscribed_user_id
3      , 4
3      , 8
1      , 4
1      , 10

SQL查询是固定的:

select events.* from events, event_subscribers
    where events.user_id = event_subscribers.subscribed_user_id and event_subscribers.user_id = 3

所有的屏蔽,过滤都是对event_subscribers这个中间表数据做操作

jasl 将本帖设为了精华贴 09月27日 07:42
zouyu 回复

很多人选择推拉结合。

一个例子就是「对僵尸用户的处理」

几年前我在薄荷网工作,那时有2000万用户,但日活只有20+万。换句话说,存在大量的僵尸用户。如果为他们准备好队列,填满内容,其实是很不划算的,浪费了大量的存储空间。尤其是使用内存数据库时,简直是烧钱。当时采用的策略是:平时不管这些僵尸用户,他们的队列是空的,没有任何 feed,当他们登录时,临时去拉数据,把他们的队列填满。

但是企业市场不能这么考虑问题,SAP 的客户每年都在付费,人家买的就是服务。所以即使客户不登录,我们还是把每个客户队列中的内容准备好。

quakewang 回复

你这个其实就是推了

xiaoronglv 回复

取消屏蔽后,是不是可以不用拉数据啊。

这个不行吧,比如用户先点取消屏蔽,再打开Feed页,那之前屏蔽的消息还是要像没屏蔽的消息一起顺次Feed,不可能不拉。

但是如果是取消屏蔽和屏蔽操作触发拉,会有很多次非顺序的写操作,不是很好。 感觉还是一开始就推比较好,毕竟屏蔽的消息数量比较少,多推并不费劲。 楼主觉得呢?

xiaoronglv 回复

不同场景 不同考虑 受教了

lithium4010 回复

和推不一样的,推的模式在产生event的时候,需要写入大量数据,而且这种设计也没有 屏蔽/取消屏蔽 导致数据不正确的问题

quakewang 回复

对屏蔽的处理,我放到了 Part 3 中。

http://mednoter.com/design-of-feed-part-three.html

  1. xiaoronglv 和 quakewang 是好友关系。
  2. quakewang 屏蔽了 xiaoronglv
  3. 当小荣吕发言时,会产生一个 event
  4. RouteService.route! 在分发时,发现 quakewang 已经屏蔽 xiaoronglv了,就不会分发给 quakewang 了。
xiaoronglv 回复

推模式问题在于任何影响订阅关系的行为,比如屏蔽或者取消屏蔽,还需要对已经推送的event进行修改

现在趋势一般向纯拉走,更容易做算法排序的 abtest 之类

ruby-china支持组功能?楼主怎么建组的?

存储部分写反了

// 分发给 Teddy (user_id: 113)
insert follow_feeds (user_id, event_id) values (333333, 113)
// 分发给 Tim(user_id: 115)
insert follow_feeds (user_id, event_id) values (333333, 115)

如果一个 event 撤销了,被用户删除了。推模式需要逐个删除feed或者标记feed?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册