feed流-后段架构一些总结

研究一下feed流,就以微博为例子

推模型

这种模型的基础就是在于,每次打开关注页,就需要读取用户所有朋友的动态,因为这种模型又称作读扩散,不需要额外的数据,只需要某个人发布动态时候写入一条记录,删除或者添加好友也只需要增删一条friends记录。每次进入关注页,都对所有关注的作者的动态进行读取,然后按时间顺序排列。但是如果是在微博这样关注无上限的平台,那么每次的拉取可能耗时将到无法接受的地步。

拉模型

一些人认为,当创作者发布文章就得写入粉丝的timeline上,用户每次读取就是读取自己的timeline,这样的模型也叫写扩散,也就是创作者发布内容时,把内容写入每个粉丝的timeline,但是缺点在于,如果有些偶像明星,粉丝量几个亿。每次这样的头部创作者发布内容的时候,都会有巨大的写入作业。而且通常写入那一刻会马上返回成功,并不是因为已经写成功了,而是投放到消息队列成功了,将会异步的写入数据库。

在线推,离线拉

推有推的好处,拉有拉的好处,推的话(粉丝读取速度快,但是工作量大,对头部创作者每次发布内容都是对服务器的考验),拉的话(逻辑很简单,但是读取效率低,关注得多了也会出现灾难),但是feed流模式下,创造的数目往往比读取的数目低很多,大部分人都是内容的消费者而非创作者。

但是真有必要把内容推给每个粉丝么?其实大部分粉丝不可能同时在线,甚至有些几天都不在线,甚至有些已经弃平台而去实际上是个僵尸。所以一个头部创造者的活跃粉丝是不多的,只把内容动态推给在线的粉丝 (这个在线的衡量标准需要计算),而当离线很久的粉丝很久之后上了平台,那么这个时候采用的是拉模型。

总结下来就是在线推,离线拉。

优化存储

在查询的过程中:

1
2
3
4
5
6
7
SELECT *
FROM articles
WHERE author_uid IN (
    ...
)
ORDER BY create_time DESC
LIMIT 20;

从articles表中选择所有列
条件是author_uid必须在一个子查询的结果集中

按文章的创建时间倒序排序

返回前20条记录

1
2
3
SELECT following_uid
FROM followings
WHERE uid = ?

从followings表中选择following_uid列。
条件是uid等于指定的用户ID (WHERE uid = ?)

这样的查询实际上就是给用户进行拉去其关注者的文章

但是无论怎么查,巨大的数据量化和读写负载对数据库而言都是不可接受的

redis

在明确了在线推,离线拉的情况下,如何再继续进行优化?

在线拉的模型下,实际单个用户的timeline就是一个缓存,可以给用户的timeline设置为数天过期的缓存,如果用户离线太久,则缓存会自动释放掉所有的东西。当他回归之时,在拉也不迟。

而且用户是否离线也可以依照这个缓存是否为空来判断。用户一般只关心最新的内容,除别有用心之人,没人会看朋友圈一周之前的内容。

reids-sortedSet

Redis的数据结构有列表List和SortedSet,对于需要按照时间顺序来判断的场景,且不可以重复那就选择SortedSet。

在线推的构建用户的个人timeline的时候,redis缓存article的id作为member唯一值,和article发布时间作为score来排序。

缓存没有数据了,或则数据量不足的时候,进行拉重建数据。

(ps:这种重复操作不影响结果的特性有个高大上的名字 ——— 幂等性)

no more标识位

而这里存在一个缓存击穿的问题,所谓的缓存击穿,就是在缓存里访问失效的数据,但是如果某个用户的清空关注者的行为,导致其根本没有timeline缓存,无法判定是失效了,还是根本就没有,还是得去数据库查询。所以设置一个标志位,no more。

对于正常缓存失效的用户,会连该no more标志位都没有,因为随着正常的失效而被释放。

而对于timeline位空的用户,会只剩下一个no more的标志位。

多层缓存

缓存不够上二三级缓存,只要是支持有序结构的 NewSQL 数据库比如 Cassandra、HBase 都可以胜任 Redis 的二级性。但是多级缓存带来性能问题,所以需要研究。

分页器

总共10篇文章在某个timeline上面,当T1时刻读取的瞬间,比如说一次读取了5个文章(即offset=0,limit=5), 读取的文章为w1, w2, w3, w4, w5。 然后T2时刻,希望读取 w6, w7 ,w8, w9, w10的时候,此时timeline添加了一篇新文章,往一下一挤,那么读取会变成 w5, w6, w7, w8, w9。那么这样是不对的。

所以这个时候思路在于offset的问题,不应该使用offset而是使用id来,比如w1-5的读取完成后,应该读取的是w6-10的文章。这样的思路是正确的,而且为了让读取是按照时间顺序,每次读取5个文章后,都应该把最后那个文章发布时间当一个标,去寻找晚于它的按时间升序5个文章。

大规模推送

对于成千上万的用户,一个mq worker显然是不够的。需要通过消息队列,把任务分化给多个mq worker,(比如worker1 对应0-1000粉,worker2对应1001-2000粉),而mq worker的dispatch只需要负责扫米列表即可。