开发设计实践:分库分表实现方案

概要

首先,我们来思考下面几个问题:

1、分库分表的常见方案有哪些?

2、基于什么维度来做分库分表?

3、分库分表之后带来了什么新的问题?

下面将基于上面的问题来进行分析:

分库分表方案

分库分表的目的是达到垂直或水平切分的目的,切分数据使其分布到不同的库或表上。最关键的点就是路由算法,把分片键(路由的key)按照指定的路由算法进行路由存放。

1、范围法 - range范围

范围法对分片键按照范围进行切分,将数据切分到不同的数据库或表上去。

  • 优点:

1)切分策略简单,根据分片键,按照范围,能够快速定位到数据在哪个库哪个表。

2)扩容简单,无需迁移数据,如果容量不够,只需增加数据库或表,扩展分片键的范围即可。

  • 缺点:

1)有热点问题。

数据量不均匀,新增的库或表,在初期的数据会比较少。

请求量不均匀,一般来说,新下的订单查询活跃度会比较高,故后面的库比前面的库负载要高,导致服务器利用率不平衡。

2、哈希法 - hash取模

哈希法对分片键采用hash取模,将数据水平切分到不同的数据库或表上去。

  • 优点:

1)切分策略简单,根据分片键,按照hash,能够快速定位到数据在哪个库哪个表。

2)数据均衡,根据分片键,数据均匀的分布在在各个库上。

3)请求均衡,根据分片键,负载均匀的分布在在各个库上。对订单进行操作时,就不会有热点问题。

热点的意思是对订单的操作集中在一个库或一个表上,其他库或表的操作很少。

  • 缺点:

1)扩容和数据迁移很麻烦。

若业务发展很好,订单量很大,那么我们就需要增加库或表,此时取模的基数会变化,变化后重新hash会导致数据迁移。

3、总结

range方案:不需要迁移数据,但有热点问题。

hash取模方案:没有热点问题,但扩容迁移数据痛苦。

那有什么方案可以做到两者的优点结合呢?,即不需要迁移数据,又能解决数据热点的问题呢?

方案思路:

hash可以解决数据均匀的问题,range可以解决数据迁移问题,那我们可以不可以两者相结合呢?利用这两者的特性呢?

我们考虑一下数据的扩容代表着,路由key(如id)的值变大了,这个是一定的,那我们先保证数据变大的时候,首先用range方案让数据落地到一个范围里面。这样以后id再变大,那以前的数据是不需要迁移的

但又要考虑到数据均匀,那是不是可以在一定的范围内数据均匀的呢?因为我们每次的扩容肯定会事先设计好这次扩容的范围大小,我们只要保证这次的范围内的数据均匀是不是就ok了。

实施分库分表之后,会带来什么新的问题呢?

以订单服务为例,使用uid进行水平切分之后,对基于uid属性的查询,可以直接路由到库,但对于非uid属性的查询,就悲剧了,由于不知道数据落在哪个库上,往往需要遍历所有库和表,性能会显著下降。

在进行讨论前,先来对业务进行简要分析,订单服务非uid属性的查询,有如下两类典型业务需求:

1、用户侧:前台访问

根据uid查询订单列表;根据订单ID查询订单。

用户侧的查询,访问量较大,服务需要高可用,并且对一致性要求较高。

2、运营侧:后台访问

通过订单ID、用户ID、商家ID、交易时间等维度来进行查询。

运营侧的查询,基本上是分页查询,访问量低,对可用性和一致性要求不高。

针对这两类不同的业务需求,应该使用什么样的方案来解决呢?

总的来说,核心思路如下:

1、用户侧:采用“建立非uid属性到uid的映射关系”的架构方案;

2、运营侧:采用“前台与后台分离”的架构方案;

用户侧,如何实施呢?

常见方法如下:

1 、映射法

  • 思路:

uid能直接定位到库,订单ID不能直接定位到库,如果通过订单ID能查询到uid,便可解决该问题。

  • 方案:

1)建立订单ID和uid的映射关系表;

2)用订单ID来查询时,先通过映射关系表查询到uid,再通过uid定位到对应的库;

3)把映射关系存放在缓存中,提高性能

4)映射关系表属性较少,可以容纳非常多数据,一般不需要分库;

5)如果数据量过大,可以通过订单ID来分库;

  • 缺点:

数据访问时,多一次网络交互,即一次cache查询。

思考:是否可以结合本地缓存来实现呢?若可以又会带来什么新的问题呢?

2、业务因子法

  • 思路:

从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个步骤,达到同一个商家下用户的订单落到相同的库和表上,这样一份订单数据,就可以支持到商家/用户/订单三个维度的查询场景。

不过这个方案存在一个缺陷:数据分布不均匀。对于头部商家其数据多,数据会集中在相同的库和表上,导致数据分布不均匀。

总结

分库分表,本质上是用空间换时间的一种手段。

你可能感兴趣的:(架构设计系列,数据库,分库分表.)