feed流 — 数据冷热分离逻辑

本文所述feed流是写扩散模型不是读扩散模型也不是推拉结合

核心思路

所有写扩散feed需要保存在磁盘中,选型mysql/hbase都可以。

活跃用户的feed需要缓存在内存中,选型redis。

翻页先走缓存,如果缓存数据不足则走磁盘。

下面称一条微博为tweet,写扩散的记录为feed。

冷热分离

第1步:冷数据写扩散

发出tweet后,写扩散到各个关注者的feed表里。

无论用什么磁盘存储,都应当使用批量提交来提升写入吞吐。

对于mysql这种单机数据库,需要按用户拆分大量数据库,才能够支撑高吞吐的写扩散操作。

第2步:热数据被动缓存

活跃用户(N天内查看过feed)经常查看feed流,非活跃用户一个月才看一次。

活跃用户查询流量直接穿透到冷数据,会耗尽冷数据服务器的文件系统缓存,导致内存换入换出频繁,性能会有很大影响。

所以,冷热数据分离势在必行。

非活跃用户冷启动,即从冷数据中捞出最新的500条feed记录,然后将500条缓存到redis中,作为热数据提供多次向后翻页的能力。

feed列表接口的逻辑是:

  • redis采用zset,用feed id或者feed时间作为score,即可实现feed增量拉取。
  • 判断zset为空,则向zset中设置一个score无穷大的占位key,即先创建出一个空集合(问题A,原因后面说)。
  • 执行expire命令,让zset在3天后自动过期,这一步应该和上一步采用pipeline确保同时生效。
  • 读mysql获取最新的500条feed,写入到zset中,并调用ZREMRANGEBYRANK保留zset中最新的500条(问题B,原因后面说),这些操作可以pipeline。

无论本次feed请求的翻页参数是什么,只要是活跃用户就应该执行上面的热数据加载逻辑。

第3步:热数据主动缓存

在feed写入冷数据后,如果关注者在redis中有热数据zset存在,那么就应该向zset推送一份,这样才能保证zset中永远是最新的500条。

主动缓存的逻辑:

  • 先让冷数据落地。
  • 判断zset是否存在,不存在则结束处理。
  • 若zset存在,则将feed添加到zset中,然后执行ZREMRANGEBYRANK确保只保留500条在zset中。

这里可以解释一下之前的问题A,为什么要先创建一个空zset再读mysql缓存到redis?

这是一个时序问题:

接口先读mysql,分发的新数据还没落地mysql,所以接口没有获取到新数据;然后分发任务把新数据写入mysql,然后判断zset不存在,所以没有主动缓存;然后,接口把mysql数据写入zset;所以,zset中数据相比mysql,少了1条最新的。

至于问题B,其实如果我们持续给一个活跃用户分发feed,那么它的zset集合肯定越来越大,所以适时的利用ZREMRANGEBYRANK去截断一下zset,可以确保zset只保留最新的500条(多一些也没大问题)。

最后

本着思维的严密性,我需要说一下上面红色的部分。

我们的热数据zset唯一的创建时机,就是当用户主动刷新feed流,并且zset不存在的情况下,才会创建一个带TTL的zset。

再看红色部分,先判断zset存在,此时如果zset恰好过期,那么接下来zadd就会导致集合被再次创建出来,并且是永久的,这就导致zset出现了第2个创建时机,显然是个bug。

所以,严谨做法应该是用lua来确保if zset existed then zadd的原子性,这样就不会导致误创建zset了。

琢磨这些玩意干啥呢?其实并发场景总是有很多的时序问题,越严谨则bug越少,系统也会健壮很多,当然对自己也是一种思维锻炼。

 

 

 

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