首先,我们来思考下面几个问题:
1、分库分表的常见方案有哪些?
2、基于什么维度来做分库分表?
3、分库分表之后带来了什么新的问题?
下面将基于上面的问题来进行分析:
分库分表的目的是达到垂直或水平切分的目的,切分数据使其分布到不同的库或表上。最关键的点就是路由算法,把分片键(路由的key)按照指定的路由算法进行路由存放。
范围法对分片键按照范围进行切分,将数据切分到不同的数据库或表上去。
1)切分策略简单,根据分片键,按照范围,能够快速定位到数据在哪个库哪个表。
2)扩容简单,无需迁移数据,如果容量不够,只需增加数据库或表,扩展分片键的范围即可。
1)有热点问题。
数据量不均匀,新增的库或表,在初期的数据会比较少。
请求量不均匀,一般来说,新下的订单查询活跃度会比较高,故后面的库比前面的库负载要高,导致服务器利用率不平衡。
哈希法对分片键采用hash取模,将数据水平切分到不同的数据库或表上去。
1)切分策略简单,根据分片键,按照hash,能够快速定位到数据在哪个库哪个表。
2)数据均衡,根据分片键,数据均匀的分布在在各个库上。
3)请求均衡,根据分片键,负载均匀的分布在在各个库上。对订单进行操作时,就不会有热点问题。
热点的意思是对订单的操作集中在一个库或一个表上,其他库或表的操作很少。
1)扩容和数据迁移很麻烦。
若业务发展很好,订单量很大,那么我们就需要增加库或表,此时取模的基数会变化,变化后重新hash会导致数据迁移。
range方案:不需要迁移数据,但有热点问题。
hash取模方案:没有热点问题,但扩容迁移数据痛苦。
那有什么方案可以做到两者的优点结合呢?,即不需要迁移数据,又能解决数据热点的问题呢?
方案思路:
hash可以解决数据均匀的问题,range可以解决数据迁移问题,那我们可以不可以两者相结合呢?利用这两者的特性呢?
我们考虑一下数据的扩容代表着,路由key(如id)的值变大了,这个是一定的,那我们先保证数据变大的时候,首先用range方案让数据落地到一个范围里面。这样以后id再变大,那以前的数据是不需要迁移的。
但又要考虑到数据均匀,那是不是可以在一定的范围内数据均匀的呢?因为我们每次的扩容肯定会事先设计好这次扩容的范围大小,我们只要保证这次的范围内的数据均匀是不是就ok了。
以订单服务为例,使用uid进行水平切分之后,对基于uid属性的查询,可以直接路由到库,但对于非uid属性的查询,就悲剧了,由于不知道数据落在哪个库上,往往需要遍历所有库和表,性能会显著下降。
在进行讨论前,先来对业务进行简要分析,订单服务非uid属性的查询,有如下两类典型业务需求:
1、用户侧:前台访问
根据uid查询订单列表;根据订单ID查询订单。
用户侧的查询,访问量较大,服务需要高可用,并且对一致性要求较高。
2、运营侧:后台访问
通过订单ID、用户ID、商家ID、交易时间等维度来进行查询。
运营侧的查询,基本上是分页查询,访问量低,对可用性和一致性要求不高。
针对这两类不同的业务需求,应该使用什么样的方案来解决呢?
总的来说,核心思路如下:
1、用户侧:采用“建立非uid属性到uid的映射关系”
的架构方案;
2、运营侧:采用“前台与后台分离”的架构方案;
常见方法如下:
uid能直接定位到库,订单ID不能直接定位到库,如果通过订单ID能查询到uid,便可解决该问题。
1)建立订单ID和uid的映射关系表;
2)用订单ID来查询时,先通过映射关系表查询到uid,再通过uid定位到对应的库;
3)把映射关系存放在缓存中,提高性能
4)映射关系表属性较少,可以容纳非常多数据,一般不需要分库;
5)如果数据量过大,可以通过订单ID来分库;
数据访问时,多一次网络交互,即一次cache查询。
思考:是否可以结合本地缓存来实现呢?若可以又会带来什么新的问题呢?
从uid中抽取业务因子
,融入到订单ID中。假设分16库,采用uid%16进行路由,uid的最后3bit决定这条数据落在哪个库上,这3bit就是所谓的业务因子
。
1)在用户下单时,根据特定函数基于uid生成3bit业务因子;
2)生成全局唯一的订单ID;
3)将3bit业务因子拼接到订单ID末尾,生成最终的订单ID;
4)按照3bit业务因子来路由到对应的库进行数据插入;
5)用订单ID来访问时,先通过特定函数从订单ID中获取到3bit业务因子,再直接定位到库进行操作。
数据可能分布不均。
思考:若生成的商户ID、用户ID、订单ID具有相同的业务因子,数据的分布是什么情况呢?是否可以在一定程度上解决运营侧的业务需求呢?
为了满足后台业务各类“奇形怪状”的需求,往往会在数据库上建立各种索引,这些索引占用大量内存,会使得用户侧前台业务uid/订单ID/商户ID上的查询性能与写入性能大幅度降低,处理时间增长。
对于这一类业务,可以采用前台与后台分离
的架构方案。
用户侧前台业务需求架构不变,运营侧后台业务需求则抽取独立的应用 / DB来支持,解除系统之间的耦合。对于业务复杂、并发量低、无需高可用、能接受一定延时的后台业务,可以不访问实时库,通过MQ或线下异步同步数据。在数据量非常大的情况下,可以采用ES搜索系统或者大数据的HIVE的设计方案来满足后台复杂多样的查询需求。
1、用户侧和运营侧应用和DB分离解耦,避免运营侧低效率查询引发用户侧查询抖动;
2、可以采用ES搜索系统或者大数据的HIVE的设计方案来满足后台复杂多样的查询需求。
还是以订单服务为例。一般的电商平台都会有两个角色,一个是用户,一个是商家,那么对应到上面的概念,用户属于用户侧,商家属于运营侧。按照上面用户侧和运营侧的架构方案:
用户侧:可以采用业务因子法,根据uid和订单ID来路由;
运营侧:可以采用分离法,通过MQ来同步数据。
可以在用户侧基于商家ID生成业务因子并加入到uid和和订单ID中,这样就可以根据商家ID、uid、订单ID来进行路由,可以解决运营侧商家维度的业务需求,当然当数据量到达一定量级,则还是要采用分离法来满足业务需求。
下面是具体实施方案:
1、基于商家ID生成一个业务因子A
2、生成用户ID时,将该业务因子A作为用户ID的一部分
3、生成订单ID时,将该业务因子A作为订单ID的一部分
4、将业务因子A作为分片键来进行分库分表
经过上面4个步骤,达到同一个商家下用户的订单落到相同的库和表上,这样一份订单数据,就可以支持到商家/用户/订单三个维度的查询场景。
不过这个方案存在一个缺陷:数据分布不均匀。对于头部商家其数据多,数据会集中在相同的库和表上,导致数据分布不均匀。
分库分表,本质上是用空间换时间的一种手段。