社交场景架构:
1.社交业务示例1.业务模型  1.发布内容(post)2.单向关注(follow)3.基于时间的内容流(timeline)follower : B关注了C,则B是C的followerfollowee : 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,followeeIdtable_user_info :userId,userInforelation表主要有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 idselect count(1) from table_followee_xx where userId = 'userB';select count(1) from table_follower_xx where userId = 'userB';b) 某用户relation 页面详细展示计算userB 的 sharding idselect followeeId from table_followee_xx where userId = 'userB';select followerId from table_follower_xx where userId = 'userB';计算Ids的sharding idselect userId,userInfo form table_user_info where userId in (#followeeId#,#followerId#)c) 某用户关注某用户计算userB 的 sharding id计算userC 的 sharding idstart transactioninsert into table_follower_xx (userId,followerId) values('userB','userC');insert into table_followee_xx (userId,followerId) values('userC','userB');commit2.问题的引入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查询缓存,未命中,查询db2.某用户(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.53.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)将最近新写入的数据加入到对应的增量列表。

]

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

  1. 蚂蚁京东新浪10位架构师424页佳作深入分布式缓存从原理到实践

    前言 最近刷到了一句耐人寻味的话,"解决雪崩问题的最好办法是不发生雪崩". 不论是在硅谷互联网公司里还是在国内的互联网平台上,曾多次遇到过海量规模的交易瞬间吞噬平台的悲惨故事. 核 ...

  2. 40张图看懂分布式追踪系统原理及实践

    前言 在微服务架构中,一次请求往往涉及到多个模块,多个中间件,多台机器的相互协作才能完成.这一系列调用请求中,有些是串行的,有些是并行的,那么如何确定这个请求背后调用了哪些应用,哪些模块,哪些节点及调 ...

  3. 传递给系统调用的数据区域太小怎么解决_40张图看懂分布式追踪系统原理及实践...

    作 者:码海 原文链接:https://mp.weixin.qq.com/s/U-8ttlVCfYtjEPOWKBHONA 前言 在微服务架构中,一次请求往往涉及到多个模块,多个中间件,多台机器的相互 ...

  4. 厉害!40 张图看懂分布式追踪系统原理及实践

    作者 | 码海 来源 | 码海 在微服务架构中,一次请求往往涉及到多个模块,多个中间件,多台机器的相互协作才能完成. 这一系列调用请求中,有些是串行的,有些是并行的,那么如何确定这个请求背后调用了哪些 ...

  5. 【存储缓存-flashcache原理及实践

    作者:[吴业亮] 博客:http://blog.csdn.net/wylfengyujiancheng flashcache,是facebook技术团队开发的新开源项目,主要目的是用SSD硬盘来缓存数 ...

  6. 这些图让你看懂分布式追踪系统原理及实践

    前言 在微服务架构中,一次请求往往涉及到多个模块,多个中间件,多台机器的相互协作才能完成.这一系列调用请求中,有些是串行的,有些是并行的,那么如何确定这个请求背后调用了哪些应用,哪些模块,哪些节点及调 ...

  7. 分布式经典书籍--深入分布式缓存 从原理到实践

  8. 分布式服务框架-原理与实践:14---流量控制-学习笔记(理论篇)

    2019独角兽企业重金招聘Python工程师标准>>> 上次学了灰度发布,这次我们学习流控. ============================================ ...

  9. 《深入分布式缓存:从原理到实践》

    喔家ArchiSelf 入行20多年来,有了一次不同寻常的尝试,虽然只是合力出了一本书. 时间回溯到2016年, 最初出于挖人的险恶用心,进入了一个名叫"中生代技术"的技术群.本以 ...

  10. 字节跳动混沌工程实践之场景化主动实验

    背景 从 2010 年 Netflix 上线 Chaos Mokey 的第一个版本到现在,虽然混沌工程发展已历时十年,但其实只在少数大厂里面有较成熟的落地,对绝大部分研发同学来说,混沌工程还是一个比较 ...

最新文章

  1. Cassandra 的数据存储结构——本质是SortedMapRowKey, SortedMapColumnKey, ColumnValue
  2. oracle 分表设计,oracle 分库分表(sharding)
  3. Mysql数据库开发的36条原则
  4. 网关Ocelot功能演示完结,久等了~~~
  5. 在没有复杂插件的情况下从Eclipse启动和调试Tomcat
  6. Mysql执行计划的extra列及filesort祥析
  7. 移动开发--移动web特别样式处理
  8. ubuntu-18.10 允许 root登录图形界面
  9. c++中istream类型到bool类型的隐式转换
  10. 连接定义点作用_【干货】我的期货交易入场点分析
  11. R语言_安装包时联网失败
  12. 高数-极限-求极限值--两个重要极限(以及拓展公式)
  13. php 正则 问号,正则表达式的问号需要怎样使用
  14. 那些惊艳的算法—时间轮任务调度(sunwind整理)
  15. 学 android需要什么基础,学习安卓开发需要什么基础?
  16. linux 浏览器问题
  17. 【附源码】计算机毕业设计Python安卓“我爱厨房”APP5loq7(源码+程序+LW+调试部署)
  18. Java pta练习题 第一章
  19. Qwt使用之QwtPlot
  20. dspic33E单片机IOPUWR复位原因之一 程序监控定时器

热门文章

  1. SpringMVC, Spring和Mybatis整合案例一
  2. ubuntu下手动安装php-amqp模块教程
  3. 酷站欣赏:12个漂亮的国外单页网站设计案例
  4. DataSetProvider的Option属性
  5. Delphi Invalidate的用法
  6. mice包--R中数据缺失值的处理
  7. 几个清华和交大学霸的公众号,值得学习
  8. 将Excel中的数据导入至sqlserver数据表
  9. HTML5新表单新功能解析
  10. yum 命令提示语法错误