2

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

 3 years ago
source link: https://mednoter.com/design-of-feed-part-two.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

Feed 后台设计文章索引

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

当用户分享一个视频时,该视频应该出现在他 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 events 中。根据好友的主键 ID 过滤 event,并且渲染给 Sam 即可。

select * from events where user_id in [1, 4, 8, 10, 33, 78, 101] order by id desc
  1. 实现简单,可以应对基本需求,比如关注,屏蔽,取消关注,取消屏蔽。

    • 假装关注:虽然我关注你了,但是我压根不想在feed中看到你。如果用拉的模式,sql 要写成这个样子

    • 屏蔽:虽然我们互相关注了,但是我屏蔽了你的动态,不想在feed中看到你。

    step1: 拉取 Sam 的好友,剔除假装关注/屏蔽的好友

     // is_faked: 假装关注
     // is_blocked: 屏蔽
     select friend_id from friendships where user_id = 3 and is_faked = 0 and is_blocked = 0
     [1, 4, 8, 10]
    

    step2: 拉取 Sam 好朋友产生的 Feed 内容。

     select * from events where user_id in [1, 4, 8, 10] order by id desc
    
  2. 关注/取消关注后,feed 中的内容会动态变化,易于维护。
  3. 与「推模式」相比,没有冗余数据,不占用存储空间。

首先,当 Sam 有1000个好友时,即使有索引,获取 SQL语句的执行效率会变低。

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

其次,无法应对企业应用中复杂且严格的权限检查。

我拿我们产品中的场景举例,部门经理 Bob 在私密的小组「人事管理」中创建了文档 「2019年薪酬调整方案」。

Bob 的这个操作,到底该出现在 Sam 的 Feed 流中吗? 由以下几个因素决定

  1. Sam 有这个 Group 的访问权限吗?

    权限信息有可能通过 rpc 获取,也有可能在非关系型数据库中。 e.g. Redis

  2. 即使 Sam 已经获得授权,也必须先接受使用条款(Term of Use),否则不能访问小组中的内容。

  3. Sam 有没有屏蔽了该小组的动态?

    如果用 sql 来构造,可能要这样写,但是有些条件可能存在 Redis 中,无论如何也是写不出来的。

     select * from events
     join groups on something
     join group_memberships on something
     join term_of_uses on something
     where user_id in [1, 4, 8, 10, 33, 78, 101] and
     condition1: sam can access this group
     condition2: sam accepted term of use
     condition3: sam doesn't block this group
     order by id desc
    

方式二:推模式。

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

比如 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. 常年不登录的僵尸用户的队列,会占用大量的存储空间,如果存储层是内存型数据库,每个月的花费不菲[1]。

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

随着产品的发展,会出现各种各样的商业场景和 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)分发至符合条件队列里,满足各种各样的业务需求。受众只需要在相应的队列里读取数据即可,查询语句十分简洁,读取效率高。

1.如何应对僵尸用户?

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

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

下一篇文章,我们来聊如何分发事件到各个队列。

吕小荣
25 September 2018

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK