本文将以“邮递中心”为例介绍“1对多”业务。切分相关架构实践:如何实现水平切分水平切分后常见问题典型问题优化思路与实践1.什么是1对多关系所谓“1对1”、“1对多”-many",""Many-to-many"来源于数据库设计中的“实体-关系”ER模型,用来描述实体之间的映射关系。1对1一个用户只有一个登录名,一个loginname只对应一个用户,一个uid对应一个login_name,一个login_name只对应一个uid,这是一对一的关系1对多一个用户可以发多条微博,一条微博有只有一个sender,一个uid对应多个msg_id,一个msg_id只对应一个uid,这是一对多的关系,多对多一个用户可以关注多个用户,一个用户也可以被关注多个粉丝,这是一个多对多的关系2.邮中心业务分析邮中心是典型的al一对多业务。一个用户可以发布多个帖子,一个帖子只对应一个发布者。任何倒闭的建筑设计都是耍流氓。我们先来看一下邮政中心对应的业务需求。帖子中心是提供帖子发布/修改/删除/查看/搜索的服务。写操作:发布(插入)post修改(update)postdelete(删除)post读操作:通过tid查询(select)post实体,单行查询query(select)userpublishedbyuidpost,listquerypostretrieval(search),例如按时间、标题和内容搜索符合条件的帖子。当数据量大,并发量大时,通常通过元数据和索引数据分离的架构来满足不同类型的需求:架构中的几个关键点要点:tiezi-center:postservicetiezi-db:提供元数据存储tiezi-search:postsearchservicetiezi-index:提供索引数据存储MQ:tiezi-center和tiezi-search的通信媒介,一般不直接使用RPC调用,而是通过MQ将两个子系统解耦(为什么这么解耦,请参考《到底什么时候该使用MQ?》)。其中tiezi-center和tiezi-search满足两种不同类型的读取需求:如上图:Query需求上的tid和uid可以被tiezi-center从metadata中读取返回给其他类型的检索需求,这可以通过tiezi-search从索引数据中检索返回对于写入需求:如上图:增加、修改、删除操作都会从tiezi-center中检索initiatetiezi-center修改metadatatiezi-center发送informationmodificationnotificationtoMQtiezi-searchacceptmodificationinformationfromMQtiezi-searchmodifiesindexdatatiezi-search,搜索架构不是本文的重点(外部索引架构设计,请参考《100亿数据1万属性数据架构设计》),以及下面的文章将重点关注帖子中心元数据的横向切分设计。3.发布中心元数据设计通过发布中心业务分析,不难理解其核心元数据为:贴子(tid,uid,time,title,content,...);其中:tid为帖子ID,主键uid为用户ID,发帖时间、标题、内容等为帖子属性库设计。在业务初期,单一数据库即可满足元数据存储需求。其典型的架构设计为:tiezi-center:post中心服务,为调用者提供友好的RPC接口tiezi-db:存储post数据,并在相关字段上建立索引,满足相关业务需求:post记录查询,通过tid查询,对账about90%ofreadrequestsselect*fromt_tieziwheretid=$tidpost列表查询,查询uid发布的所有posts,占readrequests的10%左右存储线性扩展。由于是post中心,post记录查询量占总请求量的90%,所以很容易想到对tid字段取模进行水平切分:这种方法简单直接,其优点:100%的写请求可以直接定位到90%的库读请求可以直接定位到库中。缺点:一个用户发布的所有帖子可能会落在不同的库中。通过uid查询10%的请求会很麻烦。如上图所示,一个uid访问需要遍历所有的库。5.帖子中心水平切分-uid切分法有没有一种切分的方法保证同一个用户发表的所有帖子都落在同一个库中,并且查询一个用户所有发表的帖子时,不需要去到呢?遍历所有库?答:使用uid分库可以解决这个问题。一个新问题:如果使用uid来划分数据库,保证一个用户的post数据落在同一个数据库上,那么如果通过tid查询,是不知道该post落在哪个数据库上的。难道不需要遍历整个数据库吗?如何优化库?答:tid的查询是单行记录查询。只要在数据库(或缓存)中记录tid和uid的映射关系,就可以解决这个问题。添加新的索引库:t_mapping(tid,uid);这个库只有两列,即使数据量太大也能承载很多数据,索引库可以使用tid将这种索引结构以kv的形式水平拆分,可以很好的利用Cache优化查询性能post发布后,tid和uid的映射关系不会改变,缓存的攻击率会很高使用uid分库,增加索引库记录tid和uid的映射关系uid,只要有uid上的Query:可以直接通过uid定位到库。每当有对tid的查询时:先查询索引表,通过tid查询对应的uid,再通过uid定位库。这种方式的优点:一个用户发布的帖子有10%落在同一个库,10%的请求通过uid来查询列表,可以直接定位到库索引表。缓存攻击率很高,因为tid和uid的映射关系不会改变。缺点:90%的tid请求和100%的修改请求不能直接定位到库中。需要先查询索引表。当然,这个查询是非常块状的。通常,数据可以在5ms内返回。插入时,需要对元数据和索引表进行操作,这可能会导致潜在的一致性问题。不创建索引表二次查询,是没法通过uid定位数据库的。这就是本文要介绍的“一对多”业务分库的最佳实践,遗传法。什么是分库基因?通过uid分库,假设分为16个库,使用uid%16的方法进行数据库路由。这里uid%16的本质是uid的后4位决定了这一行数据落在哪个库,这4位就是分库基因。什么是基因方法子库?在“一对多”业务场景下,使用“1”分库。生成“多”数据id时,在id末尾添加分库基因,满足“1”和“多”分库查询需求。如上图,uid=666的用户发帖(666的二进制表示为:1010011010):使用uid%16分库来决定将这行数据插入到哪个数据库中。分库基因为uid***4位,即1010生成tid时,先使用分布式ID生成算法生成前60位(上图中绿色部分),加入分库基因将tid的后4位(上图粉红色部分)组装成最终的64bitposttid(上图蓝色部分)(如何生成60bit分布式唯一ID,请参考《分布式ID生成算法》)这样就保证了同一个用户发布的所有帖子的tids都在同一个库上,tid的后4位是相同的,所以:可以通过uid%16定位到库也可以通过tid%16定位库潜在问题1:同一个uid发布的tid落在同一个库上,会不会出现数据不平衡?答:只要uid均衡,每个用户平均发帖数均衡,各个库的数据均衡。潜在问题2:一开始将16个数据库分成4位的子数据库。未来会扩展到32个数据库,分库基因变成5位。我们应该做什么?答:容量需要提前预估。256个数据库足够一年中的数据增长,所以提前预留了8bit的基因。7.总结“邮寄中心”将作为一个典型的“一对多”业务。在架构上,采用元数据和索引数据分离的架构设计方式:post服务,元数据满足uid和tid搜索服务的查询需求,索引数据满足元数据存储的复杂搜索。在数据量大的情况下,常见的分词方式有3种:tid分词法,根据tid分词库,将同一用户发表的帖子落在不同的库上,通过uid查询遍历所有库uid切分法,按照uid分库,同一用户发表的帖子落在同一个库,需要通过索引表或者缓存记录tid和uid的映射关系,通过tid查询时,先找出uid,再用uid定位库基因的方法。根据uid分库,将uid上的分库基因添加到生成的tid中,保证可以通过uid和tid直接定位到库。对于1对多的业务场景,分库架构不再是瓶颈。【本文为专栏作者《58神剑》原创稿件,转载请联系原作者】点此阅读更多该作者好文
