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

xiaoronglv for SAP · 2018年09月26日 · 最后由 quakewang 回复于 2019年01月19日 · 6017 次阅读
本帖已被管理员设置为精华贴

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 将本帖设为了精华贴。 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?

quakewang 回复

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

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
      )

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