架构设计高性能篇之分表分库

概述

在传统数据库中,当我们面临数据库的计算(CPU)和存储(IO)瓶颈时,我们可以通过横向扩展即不断地增加新的数据库来分担负载,也可以纵向扩展即提高单机的硬件配置,
但因为纵向扩展不能无限的扩展,所以实践中,常用的方式还是横向扩展,这也是分布式数据库所采用的方式。
那么如何进行横向扩展呢?答案是:分表分库,这是一种分片技术,它使传统数据库的处理能力突破了单机的限制,其集群的处理能力在某些方面丝毫不逊于分布式数据库。
这也是传统数据库在面临分布式数据库崛起时,其霸主地位依然牢不可破的原因。下面我们就来详细看看如何进行数据分片以及有那些分片策略。

方式

分表分库,就是将原本属于一个大表或一个库中的数据拆分成多个分表或多个库中,拆分后的数据可以位于同一个数据库下,也可以分布于不同的数据库中。

avatar

拆分方式有垂直拆分和水平拆分,如果拆分的对象是表,那么垂直拆分就像上图一样纵向地将一个表分成多个分表,同样水平拆分就像下图一样横向地将一个表拆分成多个分表;

avatar

而如果拆分的对象是库的话,那么垂直拆分就是以表为最小拆分单位,将表拆分到不同的数据库中,而水平拆分却有所不同,它是以行为单位,和上面以表为对象的水平拆分方式一样,只不过是将表分到不同的数据库中。
一张表被拆分成多个分表后,这些分表在物理上都是独立的,但在程序上我们还需将它们合起来看作一个表,并对这个逻辑表中的数据进行分页、排序、条件查询等操作。
这时,数据的操作效率就直接和你选择的策略有关,尤其是表的分表策略。

分表策略

在实际项目中,水平分表策略有Hash策略和范围策略,其中Hash策略比较常用而范围策略不太常用;垂直分表策略最常见的只有一种冷热策略,下面我们分别看一下这几种策略适应的场景:

Hash策略

假设,我们有一张1000万条数据的订单表Order,它有三个字段分别是Id、UserId、CreateTime,我们想将其拆分成5个分表,并将它们的后缀从0到4编好序号,如:order_0、order_1、order_2、order_3、order_4,那么我们应该如何拆分呢?

如果分表后,我们需要按Id来查询订单信息,并且该字段是使用频次最高的,那么我们可以考虑使用Hash策略,
遍历原表中的每一行数据,通过每行数据的ID值和分表数量取模,即:id%5,其取模运算的结果就是改行数据所对应表的序号,之后便将改行数据存储到分表中便可。
这样,遍历结束后,我们的1000万条数据,就分布到5个分表中了。但是,数据可能在分表中分布不均匀,这就要求id值要具有足够的散列性。

范围策略

假设,我们有一张日志表,每天将会以100万的速度增长,它也有三个字段分别是Id、LogContent、CreateTime,每天的数据最长只能保存30天,那么日志表一个月的数据量就是3000万;同样,我们想将其拆分成5个分表,并将它们的后缀从0到4编好序号,如:log_0、log_1、log_2、log_3、log_4,那么我们应该如何拆分呢?

如果分表后,我们不需要通过ID和LogContent来查询数据,只需要根据CreateTime查询某一天的日志,那么我们可以采用范围策略。
比如说从每个月按30天算,有5张表,那么30除以5每个表存6天的数据,也就是log_1存每月1-6号的数据,log_2存6-12号的数据,以此类推。
这样,我们就能按时间平均的将数据分布到每一个分表中,而且根据日期查询时,只需要将日期映射到对应表的序号,就能查找到该日期对应的数据。

冷热策略

冷热策略,其中"冷"是指数据量比较大但是通常是独立被使用的字段,比如商品详情与商品价格、商品名称相比之下,它经常是在商品详细页面而非商品列表页面用到,所以将其称为冷字段;相反,商品价格和商品名称经常会连在一起被使用,所有将其称之为"热"字段。

假设,我们有一张1000万条数据的商品表,它有四个字段分别是:id、name、price、detail,其中detail字段中的数据大小是其它三个字段的1000倍,且使用最频繁的字段却是前三个,如果仅仅是查询商品价格存在性能问题,那么我们应该如何拆分呢?

显然,这里我们面对的情况和采用Hash策略、范围策略时面对的情况不太一样,这里的查询性能问题主要是由于detail字段数据量过大,导致IO时间过长导致的。因此,我们可以将热字段id、name、price和冷字段detail分离,这样前面的热字段查询时所需加载的数据量就差不多是之前的千分之一,这样便能大大提高热字段的查询性能。

分库策略

垂直分库的方式是以表为最小拆分单位,将原属于同一个库中的表按表之间的关联性(业务模块)将它们划分到不同的数据库中,彼此隔离保持各自数据的独立性;
我这里将这种按相关性或业务模块进行拆分的策略称为"领域策略"。水平分库就是将水平分表后的数据放入不同的库中,这个我们就不多说了,主要说一下领域策略。

领域策略

假设,我们有一套电商系统,该系统由商品模块、订单模块、支付模块、登录模块构成,其中商品模块包含商品分类表(goods_category)、商品表(goods)...,订单模块包含......;
现在,因为表太多内存吃紧,导致查询效率变低,最终我们决定分库,那应该如何分库呢,分多少合适呢?

现实中,分库的时机是渐进式的——随着业务的发展,将一个库拆成两个,两个拆四个,四个拆八个。
因此,我们只能根据实际情况来确定分库的数量,如果我们分三个库,那么我们可以将支付模块、登录模块拆分出来分表放入不同的库中,因为登录模块属于基础模块其它模块对其依赖性不高,而支付模块属于支撑模块本身又不依赖其它模块比较好拆分。
数据架构即如何分库,这其实和系统架构有密切相关,而系统架构一般都是分层分域的。分层是系统横向的结构一般可以分成三层:基础层、业务领域层、应用层;分域是系统纵向的结构通常是按业务模块进行划分,模块与模块之间只通过接口交互,模块本身具有一定的独立性。

缺陷

虽然分表分库帮助我们提高了系统的性能,也让系统具备了横向扩展的能力,但是有时也会让你有杀敌一千自损八百的感觉。
比如,按ID水平分表后,我们需要按创建时间排序然后取前10条的功能,分表前的SQL语句类似于:select * from table order by create_desc limit 0,10 ,而分表后者需要聚合所有分表的数据,然后排序。
不仅排序和分页会碰到这样的问题,还有统计、连表、非拆分健的查询等等。因此,分表前得充分考虑好这些问题,再决定是否分表。
如果,万不得已要分表,那么上面的部分问题还是有解决方案的,下面是一些特定情况下常用的方案。

映射法

如果你有个用户表是按照user_id进行水平分表的,但你也需要根据用户名查询,那么你可以新将一个存放user_name和user_id对应关系的映射表;
这样,想根据用户名查询的时候,先查找映射表,获取user_id,然后再去查找分表。

基因法

如果你有个订单表采用取模算法根据order_id进行水平分表的,同时你又希望根据user_id进行查询,那么你可以将user_id的基因融入到order_id中。
意思是在生成order_id时,假如order_id是Long类型,那么其大小为64位,而分表数量是4,那么在生成订单ID时预留最后两位只生成62位的订单号,预留的两位用user_id的最后两位来补全(二进制的最后两位)。
这样,user_id的后两位被拼接到了order_id中,这时用order_id%4,或者user_id%4都可以定位当对应的分表,这是为什么呢?因为,当⼀个⼆进制的值按2^n取模时,其结果是由最后n位决定的,其它位对取模结果毫无影响。

ES+HBase

如果在使用了映射法、基因法都无法满足我们的查询需求,如:分页、排序、统计,那么最后的方案是将数据全量地导入导ES中。
那什么时候使用HBase呢?如果你的表字段很多比如60个列,其中只有10个列用作条件查询,显然为了减少ES的负载以及查询效率,我们可以只将检索的字段导入到ES中,而将全部数据导入HBase;
这样,将索引字段和数据分离,查询时就先从ES获取所查询数据的所有rowKey(主键),然后再用rowKey去Hbase中获取包含60个字段的数据,这样充分发挥了ES多条件查询的能力,也发挥了HBase大数据的存储以及列查询的能力,将两者扬长避短地结合在一起发挥最大的功用。

总结

实践中,是先分库还是先分表呢?这其实不好回答,得具体情况具体分析,但也存在一般性的拆分原则。
一般情况随着业务的增长,数据库中的表会不断增多,此时单表的数据量也不大但是表多,整体的性能不高,那么原则是先垂直分库,而垂直分库一般遵循分层分库策略;
之后,单表的量上来了,影响到性能,那么拆分原则是先垂直分库,后水平分表。

扩展阅读

架构设计思维篇之结构

架构设计思维篇之概念

架构设计容错篇之重试

架构设计容错篇之熔断

架构设计容错篇之限流

架构设计事务篇之Mysql事务原理

架构设计事务篇之CAP定理

架构设计事务篇之分布式事务

架构设计消息篇之消息丢失

架构设计消息篇之保证消息顺序性

架构设计高性能篇之分表分库

推荐阅读更多精彩内容