分库分表
-
分库:由单个数据库实例拆分成多个数据库实例,将数据分布到多个数据库实例中。就是一个数据库分成多个数据库,部署到不同机器。
-
分表:由单张表拆分成多张表,将数据划分到多张表内,就是一个数据库表分成多个表。
为什么需要分库呢?
随着业务数据量和网站QPS日益增高,对数据库压力也越来越大,单机版数据库很快会到达存储和并发瓶颈,就需要做数据库性能方面的优化.
我们知道数据库连接是有限的。在高并发的场景下,大量请求访问数据库,MySQL单机是扛不住的。微服务架构出现,就是为了应对高并发。它把订单、用户、商品等不同模块,拆分成多个应用,并且把单个数据库也拆分成多个不同功能模块的数据库(订单库、用户库、商品库),以分担读写压力。
为什么需要分表?
数据量太大的话,SQL的查询就会变慢。如果一个查询SQL没命中索引,千百万数据量的表可能会拖垮这个数据库。
即使SQL命中了索引,如果表的数据量超过一千万的话,查询也是会明显变慢的。这是因为索引一般是B+树结构,数据千万级别的话,B+树的高度会增高,查询就变慢了。
MySQL的B+树的高度怎么计算?
InnoDB存储引擎最小储存单元是页,一页大小就是16k。B+树叶子存的是数据,内部节点存的是键值+指针。索引组织表通过非叶子节点的二分查找法以及指针确定数据在哪个页中,进而再去数据页中找到需要的数据,B+树结构图如下:
假设B+树的高度为2的话,即有一个根结点和若干个叶子结点。这棵B+树的存放总记录数为=根结点指针数*单个叶子节点记录行数。
-
如果一行记录的数据大小为1k,那么单个叶子节点可以存的记录数
=16k/1k =16
. -
非叶子节点内存放多少指针呢?我们假设主键ID为bigint类型,长度为8字节(面试官问你int类型,一个int就是32位,4字节),而指针大小在InnoDB源码中设置为6字节,所以就是
8+6=14
字节,16k/14B =16*1024B/14B = 1170
因此,一棵高度为2的B+树,能存放1170 * 16=18720
条这样的数据记录。
同理一棵高度为3
的B+树,能存放1170 *1170 *16 =21902400
,大概可以存放两千万左右的记录。B+树高度一般为1-3层,如果B+到了4层,查询的时候会多查磁盘的次数,SQL就会变慢。
因此单表数据量超过千万,就需要考虑分表了。
分库分表采取的是分而治之的策略,分库目的是减轻单台MySQL实例存储压力及可扩展性,而分表是解决单张表数据过大以后查询的瓶颈问题,坦白说,这些问题也是所有关系型数据库的“硬伤”
。
常用策略包括:垂直分表
、水平分表
、垂直分库
、水平分库
。
一、朴实无华的 - 分表
1、垂直分表
垂直分表,或者叫竖着切表
,该策略是以字段为依据的!主要按照字段的活跃性、字段长度,将表中字段拆分到不同的表(主表和扩展表)中。
如果一个单表包含了几十列甚至上百列,管理起来很混乱,每次都select *
的话,还占用IO资源。这时候,我们可以将一些不常用的、数据较大或者长度较长的列拆分到另外一张表。
比如一张用户表,它包含user_id、user_name、mobile_no、age、email、nickname、address、user_desc
,如果email、address、user_desc
等字段不常用,我们可以把它拆分到另外一张表,命名为用户详细信息表。这就是垂直分表
特点:
-
每个表的结构都不一样;
-
每个表的数据也不一样,
-
有一个关联字段,一般是主键或外键,用于关联
兄弟表
数据; -
所有兄弟表的并集是该表的全量数据;
场景 :
-
有几个字段属于热点字段,更新频率很高
,要把这些字段单独切到一张表里。 -
有大字段,如text
,存储压力很大,毕竟innodb数据和索引是同一个文件;同时,我又喜欢用SELECT *,你懂得,这磁盘IO消耗的,跟玩儿似的,谁都扛不住的。 -
有明显的业务区分,或表结构设计时字段冗余。
不同业务我们要把具体字段拆开,这样有利于业务后续扩展。
2、水平分表
水平分表,也叫“横着切”。。以行数据为依据进行切分,一般按照某列的自容进行切分。 如手机号表,我们可以通过前两位或前三位进行切分,如131、132、133 → phone_131、phone_132、phone_133
,手机号有11位(100亿),量大是很正常的事儿,这样切就把一张大表切成了好几十张小表。
一张订单表,按时间range
拆分如下:
特点:
-
每个表的结构都一样;
-
每个表的数据都不一样,没有交集;
-
所有表的并集是该表的全量数据;
场景 :单表的数据量过大或增长速度很快,已经影响或即将会影响SQL查询效率,加重了CPU负担,提前到达瓶颈。
二、花里胡哨的 - 分库
需要你注意的是,传统的分库和我们熟悉的集群、主从复制可不是一个事儿
;多节点集群是将一个库复制成N个库,从而通过读写分离实现多个MySQL服务的负载均衡,实际是围绕一个库来搞的,这个库称为Master主库。而分库就不同了,分库是将这个主库一分为N,比如一分为二,然后针对这两个主库,再配置2N个从库节点。
3、垂直分库
纵向切库,太经典的切分方式,基于表进行切分,通常是把新的业务模块或集成公共模块拆分出去,比如我们最熟悉的单点登录、鉴权模块。
在业务发展初期,业务功能模块比较少,为了快速上线和迭代,往往采用单个数据库来保存数据。数据库架构如下:
但是随着业务蒸蒸日上,系统功能逐渐完善。这时候,可以按照系统中的不同业务进行拆分,比如拆分成用户库、订单库、积分库、商品库,把它们部署在不同的数据库服务器,这就是垂直分库。
垂直分库,将原来一个单数据库的压力分担到不同的数据库,可以很好应对高并发场景。数据库垂直拆分后的架构如下:
特点:
-
每个库的表都不一样;
-
表不一样,数据就更不一样了~ 没有任何交集;
-
每个库相对独立,模块化
场景 :可以抽象出单独的业务模块时,可以抽象出公共区时(如字典、公共时间、公共配置等)
4、水平分库
以行数据为依据,将一个库中的数据拆分到多个库中。
水平分库是指,将表的数据量切分到不同的数据库服务器上,每个服务器具有相同的库和表,只是表中的数据集合不一样。它可以有效的缓解单机单库的性能瓶颈和压力。
用户库的水平拆分架构如下
特点:
-
每个库的结构都一样;
-
每个库的数据都不一样,没有交集;
-
所有库的并集是全量数据;
场景 :系统绝对并发量上来了,CPU内存压力大。分表难以根本上解决量的问题,并且还没有明显的业务归属来垂直分库,主库磁盘接近饱和。
水平分库分表策略
分库分表策略一般有几种,使用与不同的场景:
-
range范围
-
hash取模
-
range+hash取模混合
1 range范围
range,即范围策略划分表。比如我们可以将表的主键,按照从0~1000万
的划分为一个表,1000~2000万
划分到另外一个表。如下图:
当然,有时候我们也可以按时间范围来划分,如不同年月的订单放到不同的表,它也是一种range的划分策略。
优点:
-
这种方案有利于扩容,不需要数据迁移。假设数据量增加到5千万,我们只需要水平增加一张表就好啦,之前
0~4000万
的数据,不需要迁移。
缺点:
-
这种方案会有热点问题,因为订单id是一直在增大的,也就是说最近一段时间都是汇聚在一张表里面的。比如最近一个月的订单都在
1000万~2000
万之间,平时用户一般都查最近一个月的订单比较多,请求都打到order_1
表啦,这就导致表的数据热点问题。
2 hash取模
hash取模策略:指定的路由key(一般是user_id、订单id作为key)对分表总数进行取模,把数据分散到各个表中。
比如原始订单表信息,我们把它分成4张分表:
-
比如id=1,对4取模,就会得到1,就把它放到第1张表,即
t_order_0
; -
id=3,对4取模,就会得到3,就把它放到第3张表,即
t_order_2
;
优点:
-
hash取模的方式,不会存在明显的热点问题。
缺点:
-
如果一开始按照hash取模分成4个表了,未来某个时候,表数据量又到瓶颈了,需要扩容,这就比较棘手了。比如你从4张表,又扩容成
8
张表,那之前id=5
的数据是在(5%4=1
,即第一张表),现在应该放到(5%8=5
,即第5
张表),也就是说历史数据要做迁移了。
3 range+hash取模混合
既然range存在热点数据问题,hash取模扩容迁移数据比较困难,我们可以综合两种方案一起,取之之长,弃之之短。
比较简单的做法就是,在拆分库的时候,我们可以先用range范围方案,比如订单id在04000万的区间,划分为订单库1,id在4000万8000万的数据,划分到订单库2,将来要扩容时,id在8000万~1.2亿的数据,划分到订单库3。然后订单库内,再用hash取模的策略,把不同订单划分到不同的表。
什么时候才考虑分库分表呢?
什么时候分表?
如果你的系统处于快速发展时期,如果每天的订单流水都新增几十万,并且,订单表的查询效率明变慢时,就需要规划分库分表了。一般B+树索引高度是2~3层最佳,如果数据量千万级别,可能高度就变4层了,数据量就会明显变慢了。
什么时候分库
业务发展很快,还是多个服务共享一个单体数据库,数据库成为了性能瓶颈,就需要考虑分库了。比如订单、用户等,都可以抽取出来,新搞个应用(其实就是微服务思想),并且拆分数据库(订单库、用户库)。
分库分表会导致哪些问题
分库分表之后,也会存在一些问题:
-
事务问题
-
跨库关联
-
排序问题
-
分页问题
-
分布式ID
1 事务问题
分库分表后,假设两个表在不同的数据库,那么本地事务已经无效啦,需要使用分布式事务了。
2 跨库关联
跨节点Join的问题:解决这一问题可以分两次查询实现
3 排序问题
跨节点的count,order by,group by以及聚合函数等问题:可以分别在各个节点上得到结果后在应用程序端进行合并。
4 分页问题
-
方案1:在个节点查到对应结果后,在代码端汇聚再分页。
-
方案2:把分页交给前端,前端传来pageSize和pageNo,在各个数据库节点都执行分页,然后汇聚总数量前端。这样缺点就是会造成空查,如果分页需要排序,也不好搞。
5 分布式ID
据库被切分后,不能再依赖数据库自身的主键生成机制了,最简单可以考虑UUID,或者使用雪花算法生成分布式ID。