12.深入分布式缓存:从原理到实践 --- 社交场景架构进化:从数据库到缓存

社交场景架构:
1.社交业务示例
	1.业务模型	
		1.发布内容(post)
		2.单向关注(follow)
		3.基于时间的内容流(timeline)

		follower : B关注了C,则B是C的follower
		followee : C被B关注,则C是B的followee

		根据每一个用户的followee,其timeline根据其中post的发表时间排序,组成了这个用户的feed。

	2.业务场景
		1.主要页面
			1.feed页
				用于展示用户的followee们发布的帖子。
				a) 这些post按照时间从新到旧的排序在页面上自上而下依次排序;
				b) feed页的内容允许分页。每一页(page)展示有限条数的post,post号从新到旧连续增加,
				page内部扔按照post时间排序。
				c) feed页是用户进入示例社交系统的首页,所以需要展示一些摘要信息。如:用户的followee个数,follower个数,timeline的post总条数等。

			2.timeline页
				timeline 用于展示指定用户发布的所有帖子。
				a) 这些帖子仍然按照时间从新到旧一次自上而下展示,过多的帖子允许分页
				b) timeline 需要展示一些摘要信息,包括本timeline从属用户的:followee个数,follower个数,本timeline的总条数

			3.relation页
				relation页展示用户的关系相关信息,包含2个子页面:
				1.followee 页,展示该用户关注的所有用户信息。
				2.follower 页,展示关注该用户的所有用户信息。

		2.主要操作
			1.与关系相关的操作
			2.与内容相关的操作

	3.业务特点
		1.海量的数据。亿级的用户数量,每个用户千级的帖子数量,平均千级的follower/followee数量
		2.高访问量。每秒十万量级的平均页面访问,每秒万级的帖子发布。
		3.用户分布非均匀。部分用户的帖子数/follower数量,相关页面访问量会超出其他用户一个数量级。
		4.时间分布的非均匀。高峰时段的访问量,数据变更量高出非高峰时期一到数个量级;高峰时段的长短也非均匀分布。存在日常的高峰时段和突发事件的高峰时段。
		5.用户+时间的非均匀分布。某个用户可能突然在某个时间点成为热点用户,其follower可能徒增数个数量级。

2.关系(relation)的存储
	1.基于DB的最简方案
	table_relation :
	id,followerId,followeeId

	table_user_info :
	userId,userInfo

	relation表主要有2个字段followerId和followeeId,一行relation记录表示用户关系拓扑的一条边,由followeerId代表的用户指向followeeId代表的用户。

		1.场景实现
			a) 展示userB的follower和followee的数量
			   select count(1) from table_relation where followerId='userB';
			   select count(1) from table_relation where followeeId='userB';
			   
			b) 展示被用户B关注的用户和关注用户B的用户列表 
			   select followeeId from table_relation where followerId = 'userB';
			   select userId,userInfo from table_user_info where userId in (#followeeId# ...);

			   select followeeId from table_relation where followeeId = 'userB';
			   select userId,userInfo from table_user_info where userId in (#followerId# ...);

			c)某用户关注/取消关注某用户
			   insert into table_relation (followerId,followeeId) values('userB','userC');
			   delete from table_relation where followerId = 'userB' and followeeId = 'userC';

		2.问题引入
			随着用户的增加,table_relation/info 表的行数膨胀。如果是亿级的用户,每个用户相关关系百级,那么table_relation的行数将膨胀到百亿级,info表膨胀到
		  亿级。由此,表的水平拆分(sharding)势在必行。
		  	对于某个用户的信息查询,首先根据userId计算出它的数据在哪个分片,再在对应分片的info表里查询到相关数据。userId到分片的映射关系有多种方式,例如hash取模等。

	2.DB的sharding方案
		将原先的relation表垂直拆分为 followee和follower表,分别记录某个用户的关注着和被关注着,接下来对followee和follower两张表基于userId进行水平拆分。

		1.场景实现
			a) 计算某用户的relation信息
				计算userB 的 sharding id
				select count(1) from table_followee_xx where userId = 'userB';
				select count(1) from table_follower_xx where userId = 'userB';

			b) 某用户relation 页面详细展示
				计算userB 的 sharding id
				select followeeId from table_followee_xx where userId = 'userB';
				select followerId from table_follower_xx where userId = 'userB';
				计算Ids的sharding id
				select userId,userInfo form table_user_info where userId in (#followeeId#,#followerId#)

			c) 某用户关注某用户
				计算userB 的 sharding id
				计算userC 的 sharding id
				start transaction
					insert into table_follower_xx (userId,followerId) values('userB','userC');
					insert into table_followee_xx (userId,followerId) values('userC','userB');
				commit

		2.问题的引入
			1.对于某些用户,他们被很多人关注,热点用户,count查询时,需要在userId上扫描的行数很多。另一方面,他们的timeline页面会被频繁的访问。
			2.当某个用户的follower较多时,通常在relation页面无法一页展示,因此需要进行分页显示,每页固定数量。然后db实现分页,扫描效率随着offset
			增加而增加,使得热点用户最后几页,变得低效。
			3.用户详细信息的展示。

	3.引入缓存
		在db层,增加冗余信息,将每个用户的关注着和被关注着的数量存入db。这样一来,对于timeline和feed页的relation摘要展示仅通过userInfo表就
	  可以完成。同时引入缓存层,需要注意的是,与关系相关的两张表,在缓存层对应的value变成了列表,这样做有2个原因:
	  	1.列表的存储使得查询可以通过一次IO完成,无需像数据库那样经过二级索引依次扫码相同的key对应的所有行。然后数据库很难做到以list作为value的类型
	  	并且很好的支撑list相关的操作。
	  	2.缓存是key-value结构,相同的userId很难分为多个key存储并且还能保证它们高效扫码(缓存的没有db中的基于key前缀的range扫码)

	  	同时userInfo和userCnt 相关信息也被分别存入不同的缓存表中,将db的一张表分为两张缓存表,原因是info信息和cnt信息的展示场景不同,不同的key的频度
	  也不同。
	  	在最上层,对访问极高的用户的info信息进行服务器端的本地缓存。

	  	a) 场景实现
	  		1.某用户(B)timeline/feed页面的relation摘要信息的展示:展示方式变成了首先根据用户B作为key查询缓存,未命中,查询db

	  		2.某用户(B)relation页面详细信息展示,分成2个子页面:follower列表展示和followee展示:
	  			1.首先同样查询followee和follower的缓存,对于频繁访问的热点用户,它的数据一定在缓存中,由此将db数据量最多,访问频度最高的用户挡在缓存外。
	  			2.对于每个用户的info信息,热点用户由于被更多的用户关注,他更有可能在详情页面被查询到,所以这类用户总是在本地缓存中能够查询的到。同时,本地
	  			缓存设置一个不长的过期时间,使得它和分布式缓存层或者数据库层的数据不会长时间不同步。过期时间的设置和存放本地缓存的服务器数量相关。
	  			3.某用户(B)关注/取消关注某用户(C)
	  				1.每一次插入/删除db的记录时,同时需要对对应的缓存的list进行变更。我们可以利用redis的list/set类型value的原子操作,在一次redis交互内
	  			  实现list/set的增删。同时在db的一个事务中,同时更新userInfo表的cnt字段。
	  			  	2.db和缓存无法共存于一个acid的事务,所以当db更新之后的缓存更新,通过在db和缓存中引入两张变更表即可保证更新事件不丢失;db每次变更时,在
	  			  db的事务中向变更表插入一条记录,同时有一个唯一的变更id或者叫版本号,随后再缓存中进行修改时,同时也设置这个版本号,再回过来删除db的这条变更
	  			  记录。如果缓存更新失败,通过引入定时任务补偿的方式保证变更一定会同步到缓存。

	  	b) 问题引入
	  		relation相关操作通过缓存和db冗余的方式基本解决了,但仍然遗留了2个问题。
	  		1.热点用户的follower详情页面查询数据量问题:热点用户由于有过长的缓存list,他们每次被查询到的时候有着极高的网络传输量,同时因为热点,它的查询频度也更
	  	    高,加重了网络传输的负担。
	  	    2.info 查询的multi-key问题仍然没有解决:虽然对热点用户本地缓存的方式避免了distributed缓存的查询,但是每个用户的follower/followee中,大部分用户
	  	    是非热点用户,它们无法利用本地缓存。由于这些非热点用户的占比更大,info的接收服务器吞吐量需求仍然没有显著减少。
	  	    3.info查询中一个重要的信息是被查询实体的followee和follower的数量,尤其是followee数量上限很高(部分热点用户存在百万甚至千万级的量),这2个cnt数量随时
	  	    变化着,为了使得查询的数值实时,系统需要在尽量间隔短的时间进行重新count,对于热点用户,如果期望实现秒级数据延迟,那么意味着每秒需要对百万甚至千万级别的数据
	  	    进行count。如何解决这些动态变化的数据的大访问量,实时性成为挑战。

	4.缓存的优化方案
		对于上述遗留的2个问题,可以通过引入增量化来解决。它对上述3个问题提供了基础,其思路是将增量数据作为一等公民(first-class),通过对增量数据的流式处理,支持relation
	  的各种查询场景,尤其是热点场景。
	  	对于本章中的示例系统,在引入系统后仍存在的热点场景如下:
	  	1.热点用户(follower很多的用户)的关系详情查询:他/她的关注着列表
	  	2.所有用户的技术相关摘要,包括热点用户的计数摘要,热点用户的follower/followee的摘要

	  	最左侧为增量事件的发起者,例如用户C关注了B和A,则产生2个follow数据,它们将分别shuffle到A和B所在的数据分片,使得a,b所在的分片中存放着它们各种相关的变更数据。这些变更
	  以某种方式按照变更产生时间排序,例如唯一主键以createTime时间戳的某种保存压缩(例如将timemillis做 36-base编码保证字母关系不变)作为前缀让db的查询实现顺序扫描,使得变更
	  数据的订阅方,如计数器或者近期增量列表能够根据变更事件的时间进行获取。

	  	1.独立的计数服务
	  		对于实时变更着的follower/followee数量的频繁查询,采用数据库的count函数实现无法保证性能和吞吐量,即便引入缓存,为了保证缓存的时效性,也会因为较短间隔的db count
	  	  查询引发性能问题。这些问题通过引入单独的计数服务使得count计算做到O(1)的查询复杂度可以得到缓解。
	  	  	计数服务可以设计成key-value结构,持久化到分布式缓存,key为用户id,value为该用户的followee/follower 数量。对于查询服务转化为缓存的某个key的简单get操作。对于value
	  	  的写入,可以利用上述增量化模块,订阅用户收到的变更事件。对于follow事件,直接对key对应的value自增操作;对于unfollow事件,则做自减事件。
	  	  	同时对增量化模块中的每个事件记录产生的版本(也可以根据时间本身自增来实现),和对计数器每个key进行版本记录,可实现去重防丢失等功能。
	  	  	每个key的更新频率取决于单位时间内针对该key的事件数量。例如对于有1亿follower的热点用户,假设每个follower每十天变更一次对某个followee的关注与否(实际上频率不会这么
	  	  频繁),那么改key的变更频率峰值为500次每秒(自然情况下的峰值约等于当天的所有访问平均分布到5~8小时内的每秒访问量),小于数据库单key的写入吞吐上限(约等于800tps)。如果考虑
	  	  批量写入的情况,则单key峰值写入会更低。

	  	2.根据事件时间排列的relation详情
	  		当需要查看某个用户的relation详情页时,涉及对follower/followee列表的分页查询。通常单个用户关注的人数数量有限,绝大多数用户在1000以内,且每次对第一页的查询频度
	  	  远高于后续分页,那么无论直接将列表存入db或者是分布式缓存,都能做到较高的吞吐量:db数据用户为二级索引,采用默认的排序时大多数情况第一页一个block可以承载;分布式缓存
	  	  时单个value可以覆盖这些followee列表。
	  	  	但是,对于热点用户的follower,情况更加复杂一些:follower的数量不可控,使得:
	  	  	a) 即便是小概率的翻页操作,对于follower很多的热点用户,仍然是高访问量的操作;且每次翻页的扫描成本很高。
	  	  	b) 单个分布式缓存的value列表无法承载过长的follower列表
	  	  	针对热点用户的follower列表查询问题,采用基于增量化的实现辅助解决。
	  	  	首先,同一个用户列表的前n页(假设为前5页)的访问概率占到总访问量的绝大部分(超过99%),而前n页的follower个数是常数个(假设每页展示20个follower,前5页只有100个follower
	  	  展示需要);其次,follower列表的展示以follow时间进行排序,最近加入的follower通常排在最前,即增量化模块的最新数据有可能放在首n页。
	  	  	基于上述假设,针对这99%访问量的前n页,可以直接查询增量数据:作为增量化的消费者每次拉去的最近前n页条变更事件直接存入热点用户的follower缓存中,对外提供查询服务。由于变更
	  	  事件既有 follow也有unfollow,无法直接确定拉取多少条,此时可根据历史的follow和unfollow数量比例对'n页条数'进行放大再拉取,之后取其中的follow事件部分存入缓存。

3.帖子(post)的存储
	1.基于db的方案
		post(postId, userId, postTime, content),对userId+postTime建立二级索引,使得查看特定用户按照时间排列的所有帖子的操作都变得更快。

		根据postId查询帖子内容:
		select * from post where postId = ?

		根据某个用户发送的帖子列表:
		select * from post where userId = ? and postTime between ? and ?

		a) 查询优化
			如果采用userId+postTime 的二级索引方式,对上述第二条查询存在严重的回表(对二级索引查到的每条记录都需要到聚簇索引中重新查询主数据)问题,降低db的吞吐量。为此,
		  可以将userId和postTime信息冗余到postId中,去掉二级索引减少回表。
		  	| userId | timeCompress | seq |
		  	| xxxxxx | xxxxxx		| xx  |
		  	postId 首6位为userId,每一位为 0~9/A~Z 36个字符中的某一个,6位可以表示21亿不同的用户,后续时间戳可以标识70年范围内的任意一秒,单个用户每秒发放的帖子不超过2位
		  seq表达的最大值。14位的postId可以适用于本设计系统的规模。其中对于timeCompress的计算,可以设计为:
		  	1.帖子发布的时间减去sns系统初次发布的时间点中间间隔的秒,进行36进制编码
		  	2.这样设计之后,timeCompress的字母顺序随时间连续递增,可以充分利用db的范围扫描
		  	对于查询某个用户发送的帖子列表的场景,sql变成了:
		  	select * from post where postId between postId1 and postID2 或者 select * from post where postId like 'userAprefix%';
		  	由于查询的是同一个用户的帖子,所以所有的postId的前缀都相同,如果查询这个用户某个时间范围的帖子,那么6位timeCompress的前面几位也相同。由于db的聚簇索引采用B+树
		  类似的存储,相同前缀的数据相邻存放,这样一来使得上述sql使用db的rangescan,避免回表造成的随机读。

		b) 吞吐量优化
			随着帖子的增加,单机db的数据量和吞吐量达到上限,由此引入水平拆分使得数据量和吞吐量线性伸缩。水平拆分以userId作为拆分字段,相同userId的数据存放在相同的db分片上。
		  由于postId的前缀中完全包含了userId的信息。所以postId可以独立作为路由运算的单元。

		c) db 方案的问题
			帖子数据以userId做拆分,但是某些热点user(假如follower数量为1亿)的读取量巨大,它们将被路由到相同的db上,后者也可能存在读取瓶颈的。为此,常见的方式为:读写分离。
		  采用1写n读,利用db自身的同步机制做主备复制。每次读取随机选取n个读库中的一个。
		  	基于读写分离的db解法存在2个问题:
		  	1.采用读写分离之后仍然存在数据延迟问题。当读库数量较多时(随着读取量水平伸缩),为保证写入的可用性,通常复制会采用异步方式进行。异步化的引入使得读库的写入时间难以
		    保证。帖子是sns的基础服务,下述的时间线同样会读取帖子,如果读库的写入延迟高于上层如时间线服务的写入,将会出现时间线上有相关postId但是查不到内容的情况。
		    2.同时sns的特点是近期数据频繁访问,较早的数据极少访问。而读写分离一旦引入,意味着每一条记录都要存储多份。当这些数据刚刚发布时,它们是较新的访问频率的数据,但是
		    随着时间的推移,它们逐渐不再被访问,但是仍然保持多分副本。假设sns系统运行10年,而只有1星期左右的数据被经常访问,那么98%的数据副本不会被读取,存储效率十分低下。
    
    2.引入服务端缓存
    	对于读多写少的场景,除了db层的读写分离,缓存也是场景的解法。对于存储效率低的问题,缓存数据的过期机制天然的避免了陈旧数据对空间的占用,所以引入缓存提升db性能成为自然
      选择。
      	1.key-value 的选型
      		缓存设计之前首先要确定一个问题:以什么作为key和value。最自然的方案当然是postId作为key,帖子内容作为value。
      		我们不难发现,同一个用户一天发出的帖子数量是有限的,通常不超过10条,平均3条左右,访问最频繁的1周以内的帖子数很少超过100条;同时单个帖子的长度是有限的(假设为1kb),
      	  那么单个用户一周发的帖子数很难超过100kb,极端情况下1mb,远低于redis单value的大小上线;同时redis这类缓存系统也支持对list型元素进行范围扫扫描。因此,缓存的key-value
      	  可以按如下方式进行设计:
      	  	1.key : userId + 时间戳
      	  	2.value : redis 的 hash 类型,field为postId,value为帖子内容
      	  	3.expire 设置为1星期,即最多同时存在2个星期的数据(假设每帖子平均长度为0.1kb,1亿用户每天发3帖子预计数量为40GB)
      	  	对于某一用户一段时间范围的查找变为针对该用户本周时间戳的hscan命令。用户发帖等操作同时同步更新db和缓存,db的变更操作记录保证一致性.

      	2.服务端缓存问题
      		引入服务端缓存利用了帖子访问频度随时间分布的局部性,降低了db的压力。同时由于失效事件的引入,减少了db副本带来的旧数据空间的浪费。但对于热点用户的查询仍然存在
      	  问题:假设热点用户的follower数据极高(1亿follower,10%的活跃),意味着这个热点用户所在的redis服务器查询的频度为1000w每秒,造成单点。

      	3.本地缓存
      		服务端缓存解决了近期数据的访问吞吐量问题,但是对于热点用户存在的单点问题,我们进一步引入本地缓存服务器缓解服务器缓存的压力。
      		对于近期发布(设为1周)的某一帖子,它所属用户的follower越多,意味着它被访问的频率越高;follower越多的用户数量越少,其发布的帖子也越少。因为帖子的访问频度随
      	  用户分布的局限性明显,所以本地缓存的目标是解决服务端缓存(近期帖子)中热点用户的访问问题。
      	  	本地缓存顶层key为用户id,value为该用户近期发布的帖子,相同key内value的逐出规则为基于时间的先进先出(较早的帖子先逐出),key键逐出规则为基于user的访问频度
      	  较少先出。
      	  	对于分散在不同服务器上的本地缓存,数据如何同步成为问题。对于帖子的新增,问题不大,因为即便本地缓存没有数据,降级为查询一次服务器缓存即可。对相同用户针对相同
      	  时间范围的查询,通过并发控制,做单单台服务器一个并发,即便对热点用户,落到服务端缓存的流量也是可控的(每台服务器一个并发)。但对于帖子的删除,情况会有些不同。当
      	  本地缓存查到有数据时,如何知道该数据是否已被删除?可以采取的解法是为每个缓存中的用户保存一个最新更新时间,当这个用户的本地缓存上次查询服务器缓存据当前超过一定
      	  时间(假设1s)时,再重新查询一次服务端缓存。同样通过并发控制,单台缓存服务器上针对热点用户的查询频度正比于服务器数量,也是可控的。
      	  	帖子的访问频度存在2个维度的非均匀性(局部性):时间的非均匀性(近期发布的帖子被访问的频度远高于早期)和发布者的非均匀性(follower多的用户的帖子被访问的频度远高于
      	  follower低的用户),且访问频度越高的帖子,在这2个维度下的数量都越少。

4.时间线(timeline)的存储
	1.基于db的方案 --- push 模式
		timeline 聚合着某个用户的followee的所有帖子,我们假设整个系统的用户总数,帖子总数庞大,并从已将水平拆分作为前提开始,讨论timeline典型的两种实现:基于push和
	  pull的实现。

	  	1.原始实现
	  		push 模式的特点是:用户每次新增一条帖子,将此帖子'推'到他/她的follower所在的db分片上,后者在每次浏览timeline时,将直接查询自己分片所存储的数据。这种模式
	  	  的表结构如下:
	  	  	timeline(userId, posterId, postId, postTime)
	  	  	其中:
	  	  		a) post表和timeline表都按照它们所属的用户进行水平拆分(相同的用户记录放在相同分片)
	  	  		b) timeline表记录着每个userId的timeline里所看到他/她关注的用户的帖子:userId就是timelien所拥有者,postId即timeline里包含的这条帖子的主键,posterId
	  	  		是发帖子人的userId,以及发帖时间postTime。
	  	  		c) timeline表的唯一约束只有一个:userId+postId。即,同一个用户下相同的一条帖子只能出现一次。

	  	  	常见操作如下:
	  	  		操作1:用户E根据时间浏览自己的timeline下一定时间范围内的帖子
	  	  		select postId from timeline where userId = E and postTime between ? and ?

	  	  		操作2:用户E follow 了新用户B:在B所在分片将B的userId 获取,并插入到E的timeline 中。
	  	  		操作3:用户E unfollow了用户B
	  	  		以主键第二字段的前缀,进行聚簇索引(主记录)的范围扫描,IO次数可控。
	  	  		delete from timeline where userId = 'E' and postId like 'B%';

	  	  		上述操作1存在以下问题:利用timeline的userId+time 联合索引可以通过范围扫描的方式获取所有timeline的主键/rowkey,所以索引本身的消耗只需少量IO。但存放
	  	  	  关键信息的postId字段不在userId+time 这个二级索引上面,意味着需要回表查询,IO次数不可控。

	  	2.push模式下的优化
	  		由于timeline相对于relation变更更加频繁,所以索引设计侧重于查询,优化如下。
	  		其中,将原有的postId字段替换为postId1,后者在格式上由(posterId+time+seq)更改为(time+posterId+seq),postId和postId1两者承载相同的信息,
	  	  可以直接互相转换。替换后,上述的操作为:
	  	  	select postId from timeline where userId = ? and postId between 'time1%' and 'time2%';
	  	  	利用前缀进行范围扫描,由于查找的是聚簇索引避免了回表,查询效率得以提升。

	  	  	上述基于db的push方案都面临一个困难的场景:E关注的B发布/删除了自己的帖子,除了修改B本身的post之外,需要插入/删除E的timeline表.假设B是一个热点用户,他拥有上亿
	  	  的follower,那么这个用户的每一次新增删除帖子的操作,都会被复制上亿次,造成增删帖子的瓶颈。

	2.基于db的方案 --- pull 模式
		和push模式不同,pull模式下用户每次新增/删除一个帖子时不需要同步到他的所有follower,所以不存在push模式下热点用户增删的瓶颈。但是每个用户查询一段时间的timeline
	  时,需要同时查询其所有的followee的近期帖子列表。

	  1.原始实现
	  	对于pull模式最简单的实现,并不需要新增timeline表,每个用户发出去的帖子都维护在post表和之前的post缓存中。pull模式下针对写的操作,没有额外的开销,但是对于更加频繁的
	  读操作(用户查看一段时间内所有followee的帖子)时,需要用户对自己的所有followee的帖子按时间进行扫描。假设每个用户平均关注500人,那么每次用户刷新timeline的时候将进行100
	  次查询(假设有100个db分片)。一个大型的社交系统,假设同时1000w人在线,平均每10s刷新一次页面,那么db的查询压力将是每秒1亿次查询,仅仅依靠db,需要上万个db实例。
	  	那么基于pull的纯db模式,怎么优化?我们发现,当平均每个用户follow了500个其他用户时,每个用户的平均follower也是500,意味着这个用户的每一条帖子,都会有500个用户会在
	  构造timeline时查询到。假设某一用户同时被多个follower的timeline查询到,那么这么并发的查询可以只访问一次db,称为代理的查询优化。假设10%的用户在线,10s刷新一次,查询
	  db一次10ms,那么同一个用户在同一时刻被代理查询优化的概率只有5%,优化效果甚微。
	  	由此可见,在pull模式下,虽然避免了热点用户的更新问题,查询效率和每个用户的followee相关,由于单个用户所follow的其他用户数量可控,查询的效果也可控,但是仍然存在优化
	  空间。

	  2.pull模式下的优化---pull/push 结合
	  	单纯基于pull的模式下,对于频繁的timeline操作,由于每个用户的500个followee分布在全部的db分片上(假设100个db分片),每个user的每次timeline查询都是100次db查询,
	  查询压力极大。而在push模式下,由于部分热点用户的存在,使得帖子发布之后的复制份数不可控。有没有一种复制份数可控,同时查询压力又尽量小?
	  	当某个用户发布了一个帖子时,只需将post同步到100个数据库分片上,假设存储这个副本的表叫做post_req,它至少需要三个字段:posterId,postId,postTime。push份数可控(
	  无论多少个follower,只复制100份);数据库按照timeline所属用户进行分片,那么每个用户所有的followee的最新帖子都落在同一个db分片上,即,每个用户每次刷新timeline,
	  只需要查询一次db查询,查询数量得到了控制。同样1000w用户同时在线,10s刷新一次,单台db分片的查询频度为每秒1w次,落在db能够承受的正常范围。
	  	虽然单台db的查询降低到了1万次每秒,但是每次查询的复杂度增加了。
	  		select * from post_req where posterId in (...平均500个id) and postTime between ? and ?
	  	如果以posterId作为索引的首字段,即便采用覆盖索引,仍然是500次左右独立的索引查询。因此采用postTime作为索引的首字段。

	  	假设每个用户平均每天发布3条帖子,且都集中在白天的8小时,系统总共1亿个用户,那么每10s将会有10w条新的post插入,每个用户500个followee,预计每20s才会在timeline中
	  出现一条新帖子。假设每次timeline更新如何查询的时间范围就是最近10s,那么采用推拉的方式,每个用户每次扫描10w条记录,却只从中选出平均0.5条新记录,仍存在一定的性能风险。
	  (500/100000000*100000) = 0.5

	  3.pull/push结合全量化查询
	  	上节提到的每次查询的postTime范围只有10s,这是基于以下假设:存在一个全量化的查询缓存支持,实现概要如下:
	  	1.对于每个用户已经查询过的timeline,可以将其存储并标注'最后查询时间',使得每次timeline刷新时只需要查询'最后查询时间'之后的记录
	  	2.对timeline查询的结果的存储可以采用缓存完成,称为timeline的最近查询缓存。缓存中可能存在已经删除掉的post,但由于缓存仅存储postId,这些刷掉的post在根据postId
	  	查询post阶段将会被过滤掉,不影响查询结果。timeline最近查询缓存通过一定的过期时间保障容量可控。

	3.增量查询引入服务端缓存
		基于上述db的方案,演进到pull/push 结合时,基本能够承载timeline的容量规模,遗留一个问题:单db实例每秒1w次,单词10w记录的索引扫描可能存在风险。我们称这剩下的
	  有风险的查询叫做'timeline增量查询'。

	  1.数据结构
	  	这个内存中的缓存保存了多个'增量列表':
	  		a) 每个列表的元素是用户ID,如A,B,C ...
	  		b) 每10秒产生一个新的列表,这10s内所有发过的帖子的用户id都在本列表内
	  		c) 同一个列表内的用户id按照字母的顺序排列,并用B+树索引

	  		作为缓存,增量查询部分不会保留数据。但由于每个用户默认保留了上次查询的结果,可以认为增量部分不会查询太老的数据,假设保留1小时。根据前面的假设(1亿用户平均每天
	  	  3条帖子),每10s大概10w条帖子,最多10w个不同的用户id,存储每个id大约占用21~55字节,缓存一个小时的数据大约占用1.6GB。

	  2.读写实现
	  	首先先看对外访问最频繁的读取操作,对外提供的服务为查询某个用户的所有followee在某段时间内是否有发帖。即,扫描某个用户的followee列表(平均500个)是否包含在扫描时间
	  范围内的这些'增量列表'中。
	  	O(MlogN)
	  	假设Log以48位底,10个节点需要3层,则每次查询需要耗时约60微秒,一个16线程的服务器每秒可以支撑25w次查询。假设每个用户10s刷新一次,取最近两个增量列表(20s范围),单台
	  这样的服务器可以支撑120w个用户的访问请求,一个大型社交系统(1亿用户10%在线)需要不超过10台缓存服务器。
	  	每当用户发布了一个帖子,他的follower需要在1~2s内看到这条帖子,而上述的每个增量列表保存的是最近10s内的数据,意味着增量列表是不断更新的。这里不妨用copy-on-write的
	  方式定期(如0.5s)将最近新写入的数据加入到对应的增量列表。

]

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值