11.2 Timeline Feed流的功能特性Timeline Feed流提供的数据应该是我们所关注的人在指定的时间段内发布的内容列表 ,并且内容按照时间由近及远排序。
用户在客户端浏览Timeline Feed页面时一般有如下两种操作方式。
下拉操作:刷新Feed流,拉取当前时间最新的N条Feed流。
上滑操作:拉取更早时间的N条Feed流。
另外,用户首次进入Timeline Feed页面时,展示的应该是当前时间最新的Feed流,与下拉操作的效果是一样的。
用户不断下拉Timeline Feed页面,就是不断地获取关注者最新发布的内容。如果在一段时间内关注者没有发布最新的内容,则会得到空数据。而不断上滑,则是不断地获取关注者更早发布的内容。虽然用户的理解是不停地上滑,就能看到很久之前的内容,但实际上几乎没有任何应用的Timeline Feed流允许用户这么做。例如微信朋友圈,一个1年都没有使用微信的用户重新登录微信,他是不可能在朋友圈中刷出这1年的好友动态的。
笔者在网上专门查询了“不停地刷朋友圈能刷到几天前”这个问题,有人实测是12天,也有人实测是30天,笔者也亲测了一次, ...
本章我们将讨论近年来各互联网产品非常核心的一个业务场景:Timeline Feed流 ,以及Timeline Feed服务的设计。本章的学习路径如下。
11.1节和11.2节分别介绍Feed流的分类,以及Timeline Feed流的功能特性。
11.3节和11.4节分别介绍以拉模式和推模式实现Timeline Feed服务。
11.5节介绍推模式和拉模式如何互补发挥优势。
11.6节详细介绍通过推拉结合模式实现Timeline Feed服务的关键技术细节,包括内容推送、收件箱设计、Timeline Feed流数据构建等。
本章关键词:推拉结合、推送子任务、ZSET、联合索引、字典序、合并。
11.1 Feed流的分类Feed流的功能在当今的互联网应用和网络社交平台中非常重要,它是一种以时间线为 基础的信息流展示形式,把用户感兴趣的内容呈现在用户的Feed页面上。如果你使用过 一些互联网应用就会发现,很多互联网应用的主页都是Feed页面,它们把Feed流当作自 己 的 “门面”。Feed流在内容聚合维度上包括但不限于如下几种形式。
推荐Feed流 :按照你的浏览兴趣聚合内容,你可 ...
10.5 基于图数据库的设计使用数据库与缓存结合实现高并发的用户关系服务是一种合格的传统方案,数据库、缓存技术都非常成熟,服务设计成本不是很高,所以在各大公司得到广泛应用。但是本节要介绍的是近年来有一定呼声的NoSQL数据库类型:图数据库。我们曾在1.10节中简单介绍过这种数据库,它以实体为点,以实体间的关系为边建立图结构,目的是更高效地描述和查询实体间的关系。用户关系服务是图数据库的典型应用场景之一,此服务本来就是用来处理用户之间的关注关系问题的,其中的用户就是图数据库的点,用户间关系就是图数据库的边。
接下来以比较知名的图数据库系统Neo4j为例,介绍如何实现用户关系服务。
10.5.1 实现用户关系Neo4j使用Cypher查询语言(CQL)执行对图数据库数据的读/写操作,CQL不仅遵循数据库SQL语法,而且具有人性化、易理解的语言格式。
每个用户在Neo4j中都是一个节点,我们使用如下CQL语句创建了8个节点分别代表用户,将用户ID作为节点的属性:
12345678CREATE (u1:User {user_id:1111111})CREATE ( ...
10.4 缓存查询虽然可以将数据库作为用户关系服务的存储选型,但是数据库毕竟是磁盘存储,其性能表现在高并发读场景中依然会遇到瓶颈。所以,本节我们在10.3节设计的基础上进一步通过缓存来优化高并发读场景的性能。
10.4.1 缓存什么数据对于一个海量用户应用来说,读取用户的关注列表和粉丝列表,以及查询用户之间的关注关系都属于高并发读场景。
大部分互联网应用在设计用户关注功能时,都会限制每个用户的最大关注数量,这是为了防止用户滥用关注功能进行刷粉、刷流量等,避免影响应用内的用户体验和社交环境。如果不限制关注数量,那么有些用户可能会通过关注大量的其他用户来获取更多的关注和粉丝,从而提高自己的曝光率。新浪微博限制一个用户最多可关注2000人。这样的限制,意味着用户关注列表的长度不会超过2000人。因此,我们可以把用户的关注列表全量缓存到Redis中,数据模型与10.2节介绍的一致。
粉丝列表则不同,对用户拥有多少粉丝是没有限制的,这就意味着粉丝列表的长度可能达到数百万人、上千万人甚至上亿人,Redis无法全量缓存这些数据。不过,对于粉丝量巨大的大V来说,大部分用户只会简单地查看粉丝列表的前几页 ...
10.3 基于数据库的设计既然存储用户关系需要大量的存储空间,那么还是使用数据库来设计方案为好。
10.3.1 最初的想法创建一个数据表来表示用户1与用户2的关注关系,数据表名为User_relation,表结构如表10-1所示。
User relation数据表的每行记录都描述了from user id对to_user_id的关注关系。细心的读者可能已经发现,这里的表结构没有提到将哪个字段作为索引。这是因为在使用数据库实现用户关系服务时,对索引的设计需要考虑一些必要的技术细节,需要经过专门讨论后才能得出最佳设计结论。
为了高效支持查询用户1是否关注了用户2,以及查询某用户的关注列表,我们首先想到的是为from_user_id字段创建索引;同样,为了高效支持查询某用户的粉丝列表,我 们也需要为to_user_id字段创建索引。那么,创建这两个索引是不是就可以了?
答案是不可以。假设我们的应用拥有1亿个用户,平均每个用户关注1000个用户,那么User_relation表将拥有至少1千亿条数据。为了支持对数据库的高性能读/写,User_relation表必然要分库分表。
分库 ...
基于Redis ZSET的设计一开始我们很容易想到使用Redis的ZSET对象来实现用户关系服务,ZSET高效支持数据的插入与查询功能,且很容易实现按照关注时间排序。
对于每个用户来说,都使用两个ZSET对象分别维护其关注列表和粉丝列表。
关注列表的Key为following_{用户ID},Member为被关注的用户ID,对应的Score为关注行为发生的时间。
粉丝列表的Key为follower_{用户ID},Member为粉丝用户ID,对应的Score为用户被关注行为发生的时间。
查询某用户的关注列表 ,就是获取following_{用户ID}集合的数据,使用ZREVRANGE命令即可实现按照关注时间从近到远排序的要求:
1ZREVRANGE following_{用户ID} 100 199
获取用户的粉丝列表也是同理,只不过读取的Key为follower_{用户ID}。
获取用户的关注数和粉丝数即获取这两个ZSET的长度,对应的Redis命令为ZCARD。
当用户1关注用户2时 ...
任何注重用户互动的互联网应用,都会将用户之间的关注功能作为产品的重要功能之一 ,因此它允许用户订阅其他用户的动态,以便及时获取用户的更新和动态。关注功能对互联网应用的重要性体现在如下。
促进社交互动:关注功能可以促进用户之间的社交互动,让用户更容易发现和关注其他用户的动态,增加用户之间的互动和交流。
个性化推荐:关注功能可以为互联网产品提供更准确的个性化推荐服务,根据用户的关注和兴趣,推荐相关的内容与服务,提升用户的满意度和体验。
增加用户黏性:关注功能可以让用户更容易发现和关注自己感兴趣的内容与服务,从而增加用户黏性,提高用户忠诚度。
无论是社交类、内容类、电商类还是其他类型的互联网应用,关注功能已经成为它们必备的功能之一。负责用户关系数据的服务就是用户关系服务,本章将按照如下学习路径来介绍用户关系服务的设计。
10.1节介绍用户关系服务的职责。
10.2节介绍使用Redis ZSET对象实现用户关系服务的方案。
10.3节介绍使用数据库实现用户关系服务的方案。
10.4节介绍应该如何设计用户关系的缓存。
10.5节介绍使用图数据库实现用户关系服务的方案。
本章关键词:关注列 ...
9.5 精确排名与粗估排名结合对于全民参与的超长排行榜,在设计功能时,产品经理基本上可以同意只展示前N名用户的详细排名,而其他用户只得知自己的名次即可的诉求。比如排行榜可以展示前10000名用户的列表,对于10000名以后的用户,并不展示其排名的前面、后面都有哪些人,只告诉其名次是多少即可。所以,实现超长排行榜的终极解决方案是ZSET精确排名与线段树粗估排名的结合:
前者负责维护前10000名用户的详细排名和排行榜列表;
后者负责展示每个用户,尤其是10000名以后的用户的粗估排名。
不过,由于线段树无法存储用户积分,所以需要使用额外的系统来做这件事情,比如第8章介绍的计数服务。
计数服务的作用是存储排行榜上的用户积分。每当更新排行榜时,直接更新计数服务,然后计数服务将积分变更事件发送到消息中间件就可以响应用户了。排行榜服务消费积分变更事件,而后持续构建ZSET和线段树,为用户提供查询排行榜列表和排名的能力。
从用户积分更新到排行榜名次变动生效的完整流程如图9-8所示。
用户发起积分更新的请求,请求中包含用户ID、排行榜名称和待增加的积分。
计数服务接收请求,请求处理逻辑本质上 ...
9.4 粗估排行榜的实现由于担心大Key会对Redis的性能产生影响,所以你所在公司的Redis维护团队可能会强行限制在使用Redis时,单个ZSET的大小不能超过10000或其他阈值。这时确实没有办法使用一个ZSET实现百万人、千万人的排行榜,只能另辟蹊径。
我们先分析排行榜产品的特点。一个几千万人参与的排行榜,前几百名之后的用户会在乎他是第80001名还是第80002名吗?实际上,这些尾部用户最多会关心其大致排名是怎样的,当他有朝一日跻身彰显名次的头部位置时,才会对名次和与上一名的差距精打细算 。
于是,我们就有了一个初步的想法:前N名用户使用ZSET精确排名,其他用户粗估排名。现在问题就可以聚焦到如何实现海量用户的粗估排名上了。
9.4.1 线段树如果放宽对排名精度的要求,那么可以通过分段思想来释放排行榜所需的大量存储空间:把积分按固定范围分成多个等长的分段,每个分段都保存当前积分处于此分段的用户数量,如图9-3所示。
请看图9-3,假设排行榜积分上限是500,现在将排行榜分为5个分段,其中第1个分段存储积分在[1, 100]范围的用户数量,第2个分段存储积分在[101, 20 ...
9.3 适用Redis实现排行榜在选定Redis ZSET数据类型后,我们开始一步步分析如何实现一个支持高并发读/写的排行榜服务。
9.3.1 适用Redis ZSET一个ZSET对象代表一个具体的排行榜,其中存储了用户ID和用户积分。
Key,排行榜名称,用于区分不同的排行榜。
Member,存储用户ID,作为排行实体。
Score,存储用户积分,便于实现按照积分排序的功能。
对于排行榜更新,可以执行ZINCRBY key score member命令,此命令用于向ZSET中的某个Member增加Score。假设ID为999的用户在角逐以千米数为积分的“跑步英雄(run_hero)”排行榜,他在某天跑了 10千米,于是需要执行如下命令为其在排行榜上加10分:
1ZINCRBY run_hero 10 999
ZINCRBY命令保证:如果member=999不在ZSET run_hero中,则将其作为新成员加入并设置初始score=10;否则,为其已有的Score增加10,正好满足排行榜的要求。
对于排行榜读取,可以使用ZREVRANGE key s ...