个人Feed流方案演进

2,249 阅读7分钟

作者:贝聊研发部-叶志伟

1、需求背景

天时人事日相催,冬至阳生春又来

作为程序员,终日废寝忘食地写代码,奈何需求源源不绝,何时是个头,该抽时间沉淀一下,于是在冬至这天,总结与分享贝聊现有的班级动态以及向个人feed流演进的方案,由于尚未实践,需求还在变更的路上,这是一场纸上谈兵式的头脑风暴。如有错误或者有更好的方法欢迎指正和指点一下。

2、现有的动态

2.1、班级动态

老师或家长发布和查看动态都是以班级为单位的,左图是老师发布动态,右图是家长查看动态,需求比较简单明了,有动态实体、点赞和回复。

表的设计如下,困难点在于亿级的数据量,我们使用的是阿里云的DRDS,详情可参考贝聊亿级数据库分库分表实践,这里不再叙述。

2.2、其他动态

考勤、食谱、广告等争先展示在用户面前,于是乎在查询出班级动态后,再查询其他数据插入到动态列表里返回给用户,顺理成章的这么做。但仔细想,动态列表的维度已经开始变细,考勤是宝宝进出幼儿园时打卡产生,已经细化到宝宝,每个家长看到的开始不一样了。
再后来需要向家长展示更多宝宝个性化的动态,继续沿用现有的设计吗?似乎可行,再去查询然后合并插入到动态列表,第一反应是,这么多RPC调用会不会很慢?继而想到的优化方案是并行去查询,打日志看速度确实有所提升。但这样子随着时间和版本迭代,这块代码越来越臃肿,而用户读写操作的比例是比较大的,每次这样去读还是比较耗费资源,有没有更优雅的方式去实现?于是个人feed流方案就此诞生。

3、个人feed流几种实现方式

本节介绍个人feed流的几种常用的实现方式,以微博为例子,有了解的同学可以跳过。

3.1、读扩散(拉模式)

用户拉取自己动态时,检索用户的关注表,然后根据关注表检索被关注者新发的动态,写入到自己的feed流。

优点:设计简单。
缺点:查询用户个人动态时检查是否需要去拉取,关注者多时很大概率需要拉取,速度慢。

3.2、写扩散(推模式)

当一个用户发微博,先往实体表里插入一条记录,同时也对应到这个用户的粉丝表,为每个粉丝插入一条动态。

优点:查询简单,从自己的feed流分页查询即可。
缺点:follower很多时,产生大量数据,写压力大,需保证都写成功。

3.3、读写结合

大V发动态,只同步发布动态给在线(或者活跃)的粉丝,离线(非活跃)的粉丝上线后,再去拉取动态,来完成推与拉。

优点:减少写扩散的压力,在线(或者活跃)用户有较快的读速度
缺点:离线(或者非活跃)用户下次上线的首次拉取速度较慢

4、方案的选择与设计

首先需要说明的是,如果实施这个方案,客户端会做改版,旧版的兼容问题会带来不少问题。

  • 一个班级相关的家长和老师数量比较少,数量在百级别。

  • 运营发布的推广范围比较广,可以指定地区、幼儿园。

  • 其他更复杂的业务场景。

单一的读和写扩散都有限制,不同的场景使用不同的方式可以更加灵活,所以选择读写结合的方式。再者,读扩散和写扩散在设计上是独立的两个模块,选择其一以后也很容易扩展。

4.1、双写

  • 旧表的主键是自增的,新的方案里为了保证动态实体和写扩散到个人feed流保持一致性和幂等性,需要生成一个全局的唯一id,如果失败了下次重新插入时不会重复。

  • 旧表是针对班级动态,新的方案里动态有多种类型,字段不满足新业务需求

  • 新业务的一些数据不能出现在旧版APP

主要基于以上这几点,决定使用新的DRDS,这会带来一个问题,旧版APP依然使用旧的接口以及旧的数据库,完全保留原来的逻辑,新版APP界面改版使用新的接口以及新的数据库,两边就需要同步数据,旧版APP产生的数据也要写到新数据库,新版APP产生的数据也要写到旧数据库,双写有很多细节的逻辑需要注意,例如数据的一致性,要用分布式事务吗?两边的同一条动态的id不一样,要做个id映射表,在同步回复和点赞时才能找到对应的动态。

4.2、表设计

初步设计的动态实体表和个人feed流表。
动态实体表可存多种类型的动态,动态的内容json格式化,以id来分库分表。
个人feed流表以member_id分库分表,用户从个人feed流表查出动态id,再从动态实体表查询。

考勤、广告等其他动态的数据有两份,一份存在各自的系统,一份在动态表里,虽然数据有冗余,在修改时需要通过mq同步,但新增和修改数据的频率比较低,冗余可提高查询的速度。

4.3、新增数据写扩散

只有在新增数据时才做写扩散,删除和编辑数据不需处理,因为个人feed流只记动态id,编辑不会有影响,如果是删除的情况下面做讨论。写数据的流程如下:

写扩散的过程可能有如下问题:

  • mq失败:消费mq时确保全部处理完后再ack,如果失败,下次重试消费。
  • 部分处理成功:mq消费的逻辑需要幂等性,重试消费时,已处理的部分不会重复。
  • 动态避免重复写:如果只是简单的insert,表是自增id的话,就会重复写。所以采用生成全局唯一id的方式,已存在的记录则跳过。
  • 写扩散比较慢,mq可能会积压,这个需要测试和预估一下。

4.4、用户主动拉取数据(读扩散)

以运营推广为例子,用户在登录时检查自己上拉取最后一条运营推广的时间,这个时间可以feed流里查出来,跟全局的最后一次运营更新时间做比较来决定是否需要更新。如果以后有其他数据需要主动拉取,做法也一样。

4.5、被删除的数据怎么处理

对于已经写入到个人feed流的动态id,如果对应的动态id被删除,则需要遍历所有相关的用户的feed去删除,代价有点大。方案是:

被删了的动态同时从feed流中删掉。

4.6、其他情况

宝宝新加入到班级,以前的动态数据是否需要扩散到该宝宝的家长的个人feed流,如果需要可以通过mq异步去实现主动拉取。

5、结束语

本文以贝聊的业务为背景介绍了个人feed流的实现方案,实际中可能比本文讲述的更复杂。万丈高楼平地起,快速迭代的节奏下不可能一步到位,所以先把基础功能实现了再慢慢完善,有机会的话,再继续介绍演进的过程。