elasticsearch实现feed流

何为feed流?

微博:用户A可以关注B,C,D,那么A可以看到B,C,D发布的聚合微博列表。

几个方案

既然是关注关系,那么一定要在数据库里维护A关注B,C,D的关系。

另外,文章自身是一定要存在数据库的,每个文章有自己的ID以及它的发布者UID。

  • pull思路:查询feed时先获取关注列表,再去数据库查出这批用户的文章。
    • 优点:实现简单,数据实时性高,一致性高。
    • 缺点:
      • 查询一批用户的文章是低效的,尤其是在数据库分库分表后,以及关注的用户很多的情况下。
      • 查询文章回来之后,还要根据发布时间聚合排序
  • push思路:B,C,D发布文章后拷贝(文章ID)给A用户,这样A用户直接查看自己的信箱即可知道feed流包含哪些文章。
    • 优点:查询高效,A用户直接查看自己的信箱,不需要根据关注关系分别去查B,C,D的文章再做聚合。
    • 缺点:
      • 需要异步任务完成文章的扇出拷贝,当被几百万人关注的情况下,拷贝过程非常缓慢。
      • A取消关注B,但是信箱里已经有B此前拷贝来的文章了。
  • pull&push思路:
    • 组合pull与push:
      • push:发布文章(仅拷贝文章ID即可)依旧拷贝给所有关注者,每个关注者的信箱由数据库改为缓存,写入效率得到提升。因为A的信箱缓存容量有限,所以总是保存最近发布的若干文章,淘汰较久的文章,一个先进先出的队列(redis zset),按发布时间排序。
      • pull:A先查看自己的信箱缓存,里面是最新的聚合列表。如果缓存里的翻页数据不够,那么蜕化为去数据库查询B,C,D们的文章,并在内存里按时间聚合,最后取出对应翻页的数据。如果A的信箱缓存还有空余的空间,那么将结果设置到缓存里,以便加速下次访问。
    • 问题:
      • 因为push仍旧产生拷贝,所以A取消关注B也应作为一个事件,由异步任务去A的信箱里删除掉B的文章。
      • A新关注B,需要将B之前的文章放入A的信箱。所以A关注B也应作为一个事件,由异步任务将B的文章按发布时间从新到旧逐条插入到A的缓存信箱中。而A的信箱因为只保留最新的聚合文章,相当于一个”大根堆”,所以只要B的文章时间新于A信箱中最新的文章就应该插入,同时如果A信箱满了那么就淘汰最老的一条文章。一个极端场景是,B的所有文章都比A信箱中的新,那么此时的终止条件就是往A信箱插入的B文章到达了A信箱的总容量。

elasticsearch方案

这个方案是2014年就已经有人实践了,相关的PPT在这里阅读

它采用pull方案实现,基于ES的terms query实现,相关的ES文档在这里阅读。

官方也给出了一个例子,下面分析一下。

首先,用户2关注了用户1、3,关系保存在index=users,type=user中:

然后,所有的文章保存在了index=tweets,type=tweet中:

上面的这条记录表示该文章是用户1发布的。

按feed原理来说,当用户2刷新自己的feed流时,应该可以看到用户1发布的这个文章:

这里对文章发起了一次查询,terms语法用于过滤user字段的值属于某个集合的记录。

在这里指定了集合的数据来自于index=users,type=user中id=2的记录的followers字段,也就是之前的:

因此,上述过滤其实就是获取来自于用户1和用户3的文章,因为用户2关注了它们。

其他

其实这个行为很像mysql中的JOIN查询,用于查询发布者属于某个集合的所有文章,只是ES是分布式系统并且擅长索引,所以整个计算的效率很高,对于使用者来说就屏蔽掉了原本大数据规模带来的查询性能问题。

不过必须注意,ES基本上是不支持JOIN的,这只是一个特例,另外一个我熟悉的特例是父子文档的过滤。

另外,在上面的PPT中其实也提到了几个实践问题,大家值得注意。

首先是使用filtered过滤而不是query匹配来执行terms语法,这样性能会更好:

另外,可以看到ES做feed的好处就是可以支持rank排序,自定义排序规则。而自己实现的话,貌似只能按照发布时间了。

另外,对关注列表的更新涉及到一个原子性问题,因为用户2关注新用户的时候可能并发调用,比如同时关注了用户1和用户3,而ES不支持增量更新一个数组,所以要用到乐观锁机制避免并发导致丢失了某个关注关系,相关文档在这里

作者还提到一个优化点,就是关闭了默认的过滤缓存,这里的过滤缓存是指将哪些文章符合该过滤条件记录下来,以便加速下一次查询。关闭的原因可能是因为缓存空间的大小不足以支撑大量的文章,所以反而增加了处理时间。

最后提一下,ES支持alias别名,好处就是可以实现在线不停机重建某个索引并完成原子切换,建议大家遵从这个规则进行部署,这样可以方便的对数据进行完整重建,应对比较大的数据规则重构。

我很久没有碰ES了,所以就不打算实践了,希望对大家有帮助。

 

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~