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

xiaoronglv for SAP · September 26, 2018 · Last by quakewang replied at January 19, 2019 · 5921 hits
Topic has been selected as the excellent topic by the admin.

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) 分发至符合条件队列里,满足各种各样的业务需求。受众只需要在相应的队列里读取数据即可,无需复杂的查询语句,大大提高了读的效率。

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

比如 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 mark as excellent topic. 27 Sep 07:42
Reply to zouyu

很多人选择推拉结合。

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

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

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

Reply to quakewang

你这个其实就是推了

Reply to xiaoronglv

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

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

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

Reply to xiaoronglv

不同场景 不同考虑 受教了

Reply to lithium4010

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

Reply to 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 了。
Reply to 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?

Reply to quakewang

关于拉的模式,虽然这样可以过滤掉屏蔽用户的全部动态,但如果只屏蔽单个动态又是一件麻烦的事情。

Reply to paicha

可以多创建一个表,记录被用户单条忽略的动态:

ignored_events
------------------------
user_id,  event_id
3      ,  10

然后加一个 not exists 或者 not in 查询

select e.* from events e, event_subscribers es
    where e.user_id = es.subscribed_user_id
      and es.user_id = 3
      and e.id not in (
        select id from ignored_events ie
          where ie.user_id = 3
      )

You need to Sign in before reply, if you don't have an account, please Sign up first.