背景

笔者新接到一个项目,是某品牌赞助的青春选秀节目打call平台,要求能够实现一个体验良好的Feed流,能够有社交广场/关注/点赞/评论等的功能。

基本思路

一条微博的信息

一条微博包括文字,图片和视频以及点赞和评论和一些基本发布时间等。
根据以上的信息,我们将其拆分为:

至此,基本的结构已经出来了。

分表

其中根据估计的用户量,主信息在千万级,图/视频/点赞/评论都在亿级,势必要进行分表。
二级索引指的是手动维护的表,比如微博主表由于要从两个维度查询,需要针对话题ID和主表ID的关系 额外建一张表,其中字段尽量少,查询时使用left
join来关联主表,以下内容皆是如此。
此种方式性能略差,可能join时可能会被解析分到多张主表,而且需要在数据更新时同步维护,所以尽量选取合适的主分片键。
微博主表:
主分片键:用户ID
二级索引:话题ID
微博图/文/视频/点赞:
主分片键:用户ID
微博评论:
主分片键:主表ID
二级索引:用户ID
微博关注:
主分片键:用户ID
二级索引:被关注用户ID

话题广场

话题广场是首页,但是由于考虑到Feed流与用户相关,话题ID只能作为二级索引,但好在话题广场数量有限且只能按时间倒序,我们可以为话题广场构建一个缓存。
每个话题拥有一个Redis中的zSet,话题ID作为value用作排序(使用雪花算法),每个消息发布时丢一份ID到对应的话题zSet,这样进入话题广场时就可以直接读取zSet来获取Feed流,
再根据ID来找到并缓存单个微博的信息。同时也要限制只能滚动不能翻页。

当然,这个队列如果过长性能会变差,所以要固定一个长度比如1000,当长度超过1020(避免每新增一条trim一次)时将列表尾部多出的部分trim掉。

关注列表

关注列表数量也不小且有双向查询的需求,用户看粉丝/粉丝看关注,正好其本身就是二级索引的结构(字段简单),直接通过建两张表来维护其关系,代价不大。

关注Feed流/用户主页

关注Feed流是个很麻烦的事情,其不仅变动频繁,而且数据在多个分片内,且关注量预估通常能够达到数百个,Feed流拉取会涉及到几乎所有表且难以缓存(用户可能会频繁刷新)。
这就要考虑如何读取数据,利用zSet可以做但这样代价比较大,尤其是有一些用户并不活跃,会造成资源浪费(内存很贵)。
这里可以用空间换时间,将每个用户的关注Feed流/用户主页用表记录下来。

理论上还需要一个发件箱,但是主表的分片本就是发件人ID,所以可以用主表用做发件箱。

部分写扩散

写扩散是指将一个up主发布的一条Feed推送给每一个关注者,这样对于读取性能会大幅增加,但是会带来巨量的数据。比如一个1kw关注量的up主的一条微博会被写入到1kw粉丝的收件箱中,
而且删除/关注关系的变动会导致同样1kw的变动,但是如何能够维护一个一定量大小的个人收件箱,比如1000条,参考话题广场那样定期trim,也是不错的选择。

同时,对于新增/取消关注的情况,也需要根据当前收件箱的最旧一条数据拉取新关注up主的近期微博写入/删除到收件箱,此情况数据量较小,范围可控。

缓存

由于收件箱信息量较小,在初次拉取或刷新时可以一次性从收件箱中读取较多的比如100条数据形成一个快照丢到Redis队列中并设置一个过期时间,后续的滚动就可以从Redis中获取。第一次读取或刷新
时更新快照,并且滚动时根据ID定位。队列用完时再从收件箱中小批量读取。

但是发件箱,也就是用户主页不建议如此,因为会有多个用户访问该up主主页,一个快照的话会更新不及时,多个快照会浪费内存。当然也可以根据粉丝量为某些用户开启专属的zSet,实时更新。

PS:此思路来自于观察微博的刷新用时较久猜测而来,实测性能不错。

补充读拉取

收件箱长度有限,如果真的有用户花了两个小时把收件箱翻完了该怎么办,这时就只能降级到读拉取了。
用户的关注量通常较多,远大于直接使用in的建议范围,所以这里只能是分批多次in来查询后内存排序,查询次数很多,涉及表也很多,但这是没什么办法的办法。
即使增加收件箱长度,也终究是有可能需要读拉取。

点赞

害,写累了。
点赞其实也是个很麻烦的事情,热门帖子读取和点赞变动频繁,读写冲突,且点赞量没办法合并缓存。
好在用户对此不够敏感(毕竟不需要在界面上实时更新),那么就可以使用一个redis key单独缓存(要设置过期时间)。

点赞时客户端按钮亮起(欺骗用户),同时发起异步请求,校验重复点赞后发送MQ更新点赞信息。

MQ中使用分区顺序和批量消息来merge请求,可以降低热点数据频繁更新的次数。
合并后的数据更新到主表上,同时在redis更新主表中的点赞数量(HASH结构),在这里更新使用直接更新当前最新数量而不是incr来避免并发更新丢失,也就是最终一致性。

最后,评论等就不再赘述了,冗余数据后可无需缓存。