MySQL的分区/分库/分表总结

前言

本文一切基于MySql InnoDB

概念

  1. 分区:把一张表的数据分成多个区块,在逻辑上看最终只是一张表,但底层是由多个物理区块组成的

  2. 分表:把一张表按一定的规则分解成多个具有独立存储空间的实体表。系统读写时需要根据定义好的规则得到对应的子表名,然后操作它。

  3. 分库:把一个库拆成多个库,突破库级别的数据库操作I/O瓶颈。

在mysql中,schema和库(database)是一个概念

数据库架构演变

一开始,我们只用单机数据库就足够满足业务需要了,但随着业务的拓展,带来越来越多的请求,我们将数据库的写操作和读操作进行分离,使用多个从库副本(Slaver Replication)负责读,使用主库(Master)负责写。从库从主库同步更新数据,保持数据一致。架构上的数据库主从同步,使得从库可以水平扩展,所以更多的读请求不成问题。

但是当用户量级上来后,写请求越来越多,该怎么办?加一个Master是不能解决问题的, 因为数据要保存一致性,写操作需要2个master之间同步,相当于是重复了,而且更加复杂。

这时就需要用到分库分表,对写操作进行切分。

什么情况下要分库分表

任何问题都是太大或者太小的问题,我们这主要面对的是数据量太大的问题。

  1. 用户请求量太大

    • 瓶颈:单服务器的TPS,内存,IO都是有限的。
    • 解决方法:分散请求到多个服务器上; 其实用户请求和执行一个sql查询是本质是一样的,都是请求一个资源,只是用户请求还会经过网关,路由,http服务器等。
  2. 单表数据量太大

    • 瓶颈:索引膨胀,查询耗时长,影响正常CRUD。
    • 解决方法:切分成多个数据集更小的表。
  3. 单库数据量太大

    • 瓶颈:单个数据库处理能力有限,单库所在服务器上磁盘空间不足,I/O有限;
    • 解决方法:切分成更多更小的库

1 分区

首先,我们要明白分区的区是指什么!

我们在《【InnoDB详解二】MySQL文件系统和InnoDB存储结构》一文中提到过,MySQL的物理数据,存储在表空间文件(.ibdata1和.ibd)中,这里讲的分区的意思是指将同一表中不同行的记录分配到不同的物理文件中,几个分区就有几个.idb文件

MySQL在5.1时添加了对水平分区的支持。分区是将一个表或索引分解成多个更小,更可管理的部分。每个区都是独立的,可以独立处理,也可以作为一个更大对象的一部分进行处理。这个是MySQL支持的功能,业务代码无需改动。

可以通过使用SHOW VARIABLES命令来确定MySQL是否支持分区,如:

1
2
3
4
5
6
7
8
mysql> SHOW VARIABLES LIKE '%partition%';

+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| have_partition_engine | YES |
+-----------------------+-------+
1 row in set (0.00 sec)

在如上列出的一个正确的SHOW VARIABLES 命令所产生的输出中,如果没有看到变量have_partition_engine的值为YES,那么MySQL的版本就不支持分区。

MySQL是面向OLTP的数据库,对于分区的使用应该更加小心,如果不清楚如何使用分区可能会对性能产生负面的影响。

1.1 MySQL分区类型

目前MySQL支持一下几种类型的分区:

  1. RANGE分区:基于一个给定区间边界,得到若干个连续区间范围,按照分区键的落点,把数据分配到不同的分区;
  2. LIST分区:类似RANGE分区,区别在于LIST分区是基于枚举出的值列表分区,RANGE是基于给定连续区间范围分区;
  3. HASH分区:基于用户自定义的表达式的返回值,对其根据分区数来取模,从而进行记录在分区间的分配的模式。这个用户自定义的表达式,就是MySQL希望用户填入的哈希函数。
  4. KEY分区:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且使用MySQL 服务器提供的自身的哈希函数。

如果表存在主键或者唯一索引时,分区列必须是唯一索引的一个组成部分。

在实战中,十有八九都是用RANGE分区。

1.1.1 RANGE分区

RANGE分区是实战最常用的一种分区类型,行数据基于属于一个给定的连续区间的列值被放入分区。

但是记住,当插入的数据不在一个分区中定义的值的时候,会抛异常。

RANGE分区主要用于日期列的分区,比如交易表啊,销售表啊等。可以根据年月来存放数据。

如果你分区走的唯一索引中date类型的数据,那么注意了,优化器只能对YEAR(),TO_DAYS(),TO_SECONDS(),UNIX_TIMESTAMP()这类函数进行优化选择。

实战中可以用int类型的字段来存时间戳做分区列,那么只用存yyyyMM就好了,也不用关心函数了。

MySQL使用PARTITION命令来做分区,sql语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE
`Order` (
`id`
INT NOT NULL AUTO_INCREMENT,
`partition_key`
INT NOT NULL,
`amt`
DECIMAL(5) NULL) PARTITION BY RANGE(partition_key)
PARTITIONS 5(
PARTITION part0 VALUES LESS THAN(201901),
PARTITION part1 VALUES LESS THAN(201902),
PARTITION part2 VALUES LESS THAN(201903),
PARTITION part3 VALUES LESS THAN(201904),
PARTITION part4 VALUES LESS THAN(201905),
PARTITION part4 VALUES LESS THAN MAXVALUE;

RANGE分区通过使用PARTITION BY RANGE(expr)实现,其中“expr” 可以是某个列值,如id,no,partition_key等。或一个基于某个列值并返回一个整数值的表达式,如YEAR(date)。不过值得注意的是,expr的返回值,不可以为NULL。

其中,MAXVALUE 表示最大的可能的整数值。如果没有设置MAXVALUE这个分区,那么此时如果insert一个partition_key大于201905的记录,MySQL就会抛出异常,插入失败。

VALUES LESS THAN的排列必须从小到大顺序列出,这样MySQL才能识别一个一个的区间段。

这时候我们先插入一些数据:

1
2
3
INSERT INTO `Order` (`id`, `partition_key`, `amt`) VALUES ('1', '201901', '1000');
INSERT INTO `Order` (`id`, `partition_key`, `amt`) VALUES ('2', '201902', '800');
INSERT INTO `Order` (`id`, `partition_key`, `amt`) VALUES ('3', '201903', '1200');

现在我们查询一下,通过EXPLAIN PARTITION命令发现SQL优化器只需搜对应的区,不会搜索所有分区:

因为partition_key是分区键。当然,我们也可以直接指定搜索哪个分区:

SELECT * FROM Order PARTITION (part0,part1) WHERE status amt > 1000

注意,如果sql语句不指定分区,则会走所有分区,性能反而会不升反降。所以分区表后,select语句必须走分区键。

涉及聚合函数SUM()、COUNT()的查询时,如果不指定分区,那么会在每个分区上并行处理。例如执行这条语句SELECT COUNT(1) FROM Order,则会在每个分区上都同时运行查询;

一个例子不够,我们再举一个例子,来看看expr是个函数表达式的场景,假如现在有如下雇员表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE employees (
id INT NOT NULL,
fname VARCHAR(30),
lname VARCHAR(30),
hired DATE NOT NULL DEFAULT '1970-01-01',
separated DATE NOT NULL DEFAULT '9999-12-31',
job_code INT,
store_id INT
)
PARTITION BY RANGE (YEAR(separated)) (
PARTITION p0 VALUES LESS THAN (1991),
PARTITION p1 VALUES LESS THAN (1996),
PARTITION p2 VALUES LESS THAN (2001),
PARTITION p3 VALUES LESS THAN MAXVALUE
);

在这个方案中,在1991年前离职的所有雇员的记录保存在分区p0中,1991年到1995年期间离职的所有雇员的记录保存在分区p1中, 1996年到2000年期间离职的所有雇员的记录保存在分区p2中,2000年后离职的所有工人的信息保存在p3中。

当需要删除“旧的”数据时,使用分区会有意想不到的效果。

假如我们想删除所有在1991年前就已经离职的雇员的记录,你只需简单地使用ALTER TABLE employees DROP PARTITION p0;

对于有大量行的表,这比运行DELETE FROM employees WHERE YEAR(separated) <= 1990要有效得多。

1.1.2 LIST分区

MySQL中的LIST分区在很多方面类似于RANGE分区。和RANGE分区一样,LIST分区的每个分区必须明确定义。它们的主要区别在于,LIST分区是基于枚举出的值列表分区,RANGE是基于给定连续区间范围分区;

LIST分区通过使用PARTITION BY LIST(expr)来实现,我们假定有20个音像店,分布在4个有经销权的地区,如下表所示:

地区 商店ID 号
北区 3, 5, 6, 9, 17
东区 1, 2, 10, 11, 19, 20
西区 4, 12, 13, 14, 18
中心区 7, 8, 15, 16

要按照属于同一个地区商店的记录保存在同一个分区的原则来分割表,可以使用下面的CREATE TABLE语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE employees (
id INT NOT NULL,
fname VARCHAR(30),
lname VARCHAR(30),
hired DATE NOT NULL DEFAULT '1970-01-01',
separated DATE NOT NULL DEFAULT '9999-12-31',
job_code INT,
store_id INT
)
PARTITION BY LIST(store_id)
PARTITION pNorth VALUES IN (3,5,6,9,17),
PARTITION pEast VALUES IN (1,2,10,11,19,20),
PARTITION pWest VALUES IN (4,12,13,14,18),
PARTITION pCentral VALUES IN (7,8,15,16)
);

可以看到,和RANGE分区相比,LIST分区的VALUES IN后面接的是枚举值列表,不像RANGE是用VALUES LESS THAN来定义区间边界。

如果试图插入字段值(或分区表达式的返回值)不在分区值列表中的任何一行时,那么“INSERT”查询将失败并报错。例如,假定LIST分区的采用上面的方案,那么下面的查询将失败:INSERT INTO employees VALUES (224, 'Linus', 'Torvalds', '2002-05-01', '2004-10-12', 42, 21);。因为“store_id”字段值21不能在用于定义分区pNorth, pEast, pWest,或pCentral的值列表中找到。

要重点注意的是,LIST分区没有类似如“VALUES LESS THAN MAXVALUE”这样的包含其他值在内的定义。所以将要匹配的任何值都必须在值列表中能够找到。

1.1.3 HASH分区

HASH分区主要用来确保数据在预先确定数目的分区中平均分布。在RANGE和LIST分区中,我们必须明确指定一个给定的区间或列值集合,来指定哪些记录进入哪些分区;而在HASH分区中,MySQL自动完成分配记录到区间的工作,你所要做的只是确定一个用来做哈希的字段或者表达式,以及指定被分区的表将要被分割成的分区数量。

要使用HASH分区来分割一个表,要在CREATE TABLE 语句上添加一个PARTITION BY HASH (expr)子句,其中“expr”同样可以是一个返回一个整数的表达式,或者仅仅是字段类型为整型的某个字段。

此外,你很可能需要在后面再添加一个“PARTITIONS num”子句,其中num 是一个非负的整数,它表示表将要被分割成分区的数量。

例如,下面的语句创建了一个使用基于“store_id”列进行哈希处理的表,该表被分成了4个分区:

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE employees (
id INT NOT NULL,
fname VARCHAR(30),
lname VARCHAR(30),
hired DATE NOT NULL DEFAULT '1970-01-01',
separated DATE NOT NULL DEFAULT '9999-12-31',
job_code INT,
store_id INT
)
PARTITION BY HASH(store_id)
PARTITIONS 4;

如果没有包括一个PARTITIONS子句,那么分区的数量将默认为1。

注意,expr不应该设置的过于复杂,因为每当插入或更新(或者可能删除)一行时,这个表达式都要计算一次;这意味着非常复杂的表达式可能会引起性能问题,尤其是在执行同时影响大量行的运算(例如批量插入)的时候。

最有效率的哈希函数是只对单个表列进行计算,并且它的结果值随字段值进行一致地增大或减小,因为这考虑了在分区范围上的“修剪”。也就是说,表达式值和它所基于的列的值变化越接近,MySQL就可以越有效地使用该表达式来进行HASH分区。

当使用了“PARTITION BY HASH”时,MySQL将基于用户提供的函数结果的模数来确定使用哪个编号的分区。换句话,对于一个表达式“expr”,将要保存记录的分区编号为N ,其中“N = MOD(expr, num)”。

1.1.4 KEY分区

按照KEY进行分区类似于按照HASH分区,除了HASH分区使用的用户定义的表达式,而KEY分区的哈希函数是由MySQL 服务器提供。

MySQLCluster使用函数MD5()来实现KEY分区;对于使用其他存储引擎的表,服务器使用其自己内部的 哈希函数,这些函数是基于与PASSWORD()一样的运算法则。

“CREATE TABLE … PARTITION BY KEY”的语法规则类似于创建一个通过HASH分区的表的规则。它们唯一的区别在于使用的关键字是KEY而不是HASH,并且KEY分区只采用一个或多个列名的一个列表。

1
2
3
4
5
6
7
CREATE TABLE tk (
col1 INT NOT NULL,
col2 CHAR(5),
col3 DATE
)
PARTITION BY LINEAR KEY (col1)
PARTITIONS 3;

1.1.5 子分区

子分区是分区表中每个分区的再次分割。例如,考虑下面的CREATE TABLE 语句:

1
2
3
4
5
6
7
8
9
CREATE TABLE ts (id INT, purchased DATE)
PARTITION BY RANGE(YEAR(purchased))
SUBPARTITION BY HASH(TO_DAYS(purchased))
SUBPARTITIONS 2
(
PARTITION p0 VALUES LESS THAN (1990),
PARTITION p1 VALUES LESS THAN (2000),
PARTITION p2 VALUES LESS THAN MAXVALUE
);

表ts有3个RANGE分区。这3个分区(p0, p1, 和 p2)中的每一个分区又被进一步分成了2个子分区。实际上,整个表被分成了3 * 2 = 6个分区。但是,由于PARTITION BY RANGE子句的作用,p0分区的子分区里,只会保存“purchased”列中值小于1990的那些记录。

在MySQL 5.1中,对于已经通过RANGE或LIST分区了的表再进行子分区是可能的。子分区既可以使用HASH希分区,也可以使用KEY分区。这也被称为复合分区(composite partitioning)。

为了对个别的子分区指定选项,使用SUBPARTITION 子句来明确定义子分区也是可能的。例如,创建在前面例子中给出的同一个表的、一个更加详细的方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE TABLE ts (id INT, purchased DATE)
PARTITION BY RANGE(YEAR(purchased))
SUBPARTITION BY HASH(TO_DAYS(purchased))
(
PARTITION p0 VALUES LESS THAN (1990)
(
SUBPARTITION s0,
SUBPARTITION s1
),
PARTITION p1 VALUES LESS THAN (2000)
(
SUBPARTITION s2,
SUBPARTITION s3
),
PARTITION p2 VALUES LESS THAN MAXVALUE
(
SUBPARTITION s4,
SUBPARTITION s5
)
);

不过有几点要注意的语法项:

  1. 每个分区必须有相同数量的子分区。
  2. 如果在一个分区表上的某个分区上使用SUBPARTITION来明确定义子分区,那么就必须定义其他所有分区的子分区。

子分区可以用于特别大的表,在多个磁盘间分配数据和索引。假设有6个磁盘,分别为/disk0, /disk1, /disk2等。现在考虑下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
CREATE TABLE ts (id INT, purchased DATE)
PARTITION BY RANGE(YEAR(purchased))
SUBPARTITION BY HASH(TO_DAYS(purchased))
(
PARTITION p0 VALUES LESS THAN (1990)
(
SUBPARTITION s0a
DATA DIRECTORY = '/disk0'
INDEX DIRECTORY = '/disk1',
SUBPARTITION s0b
DATA DIRECTORY = '/disk2'
INDEX DIRECTORY = '/disk3'
),
PARTITION p1 VALUES LESS THAN (2000)
(
SUBPARTITION s1a
DATA DIRECTORY = '/disk4/data'
INDEX DIRECTORY = '/disk4/idx',
SUBPARTITION s1b
DATA DIRECTORY = '/disk5/data'
INDEX DIRECTORY = '/disk5/idx'
),
PARTITION p2 VALUES LESS THAN MAXVALUE
(
SUBPARTITION s2a,
SUBPARTITION s2b
)
);
  • DATA DIRECTORY表示数据的物理文件的存放目录
  • INDEX DIRECTORY表示索引的物理文件的存放目录

在这个例子中,每个RANGE分区的数据和索引都使用一个单独的磁盘。存储的分配如下:

  1. 购买日期在1990年前的记录占了大量的存储空间,所以把它分为了四个部分进行存储,组成p0分区的两个子分区(s0a 和s0b)的数据和索引都分别用一个单独的磁盘进行存储。换句话说:
    • 子分区s0a 的数据保存在磁盘/disk0中。
    • 子分区s0a 的索引保存在磁盘/disk1中。
    • 子分区s0b 的数据保存在磁盘/disk2中。
    • 子分区s0b 的索引保存在磁盘/disk3中。
  2. 保存购买日期从1990年到1999年间的记录(分区p1)不需要保存购买日期在1990年之前的记录那么大的存储空间。这些记录分在2个磁盘(/disk4和/disk5)上保存,而不是4个磁盘:
    • 属于分区p1的第一个子分区(s1a)的数据和索引保存在磁盘/disk4上 — 其中数据保存在路径/disk4/data下,索引保存在/disk4/idx下。
    • 属于分区p1的第二个子分区(s1b)的数据和索引保存在磁盘/disk5上 — 其中数据保存在路径/disk5/data下,索引保存在/disk5/idx下。
  3. 保存购买日期从2000年到现在的记录(分区p2)不需要前面两个RANGE分区那么大的空间。当前,在默认的位置能够足够保存所有这些记录。

1.2 分区维护

MySQL提供了许多修改分区表的方式。添加、删除、重新定义、合并或拆分已经存在的分区是可能的。所有这些操作都可以通过使用ALTER TABLE命令的分区扩展来实现。

下面我们来总结一下所有分区维护的命令,为简便计,我们定义几种partitions_exprs来替代如下子命令:

  • RANGE分区,range_partitions_exprs(n)即为:
    1
    2
    PARTITION p VALUES LESS THAN (xxx)
    ...(n个PARTITION子句)
  • LIST分区,list_partitions_exprs即为:
    1
    2
    PARTITION p VALUES IN (xxx,yyy,...),
    ...(n个PARTITION子句)

1.2.1 添加分区

  1. 为已创建的未分区表创建分区:
    • RANGE:ALTER TABLE tb PARTITION BY RANGE (expr) ( range_partitions_exprs(n>0) );
    • LIST:ALTER TABLE tb PARTITION BY LIST (expr) ( list_partitions_exprs(n>0) );
    • HASH:ALTER TABLE tb PARTITION BY HASH(expr) PARTITIONS 2;
    • KEY:ALTER TABLE tb PARTITION BY KEY(expr) PARTITIONS 2;
  2. 为分区表添加n个分区:
    • RANGE:ALTER TABLE tb ADD PARTITION ( range_partitions_exprs(n>0) );
    • LIST:ALTER TABLE tb ADD PARTITION ( list_partitions_exprs(n>0) );
    • HASH & KEY:ALTER TABLE tb ADD PARTITION PARTITIONS n;

对于通过RANGE分区的表,只可以使用ADD PARTITION添加新的分区到分区列表的尾端。设法通过这种方式在现有分区的前面或之间增加一个新的分区,将会导致报错。此时建议使用下文的拆分操作,REORGANIZE命令可以运行expr重叠。

不能添加这样一个新的LIST分区,该分区包含有已经包含在现有分区值列表中的任意值。如果试图这样做,将会导致错误。此时建议使用下文的拆分操作,REORGANIZE命令可以运行expr重叠。

1.2.2 重调整分区

  1. 数据不丢失的前提下,将m个分区合并为n个分区(m>n),即减量重新组织分区
    • RANGE:ALTER TABLE tb REORGANIZE PARTITION s0,s1,... INTO ( range_partitions_exprs(n) )
    • LIST:ALTER TABLE tb REORGANIZE PARTITION s0,s1,... INTO ( list_partitions_exprs(n) )
    • HASH & KEY:ALTER TABLE clients COALESCE PARTITION n; (n小于原有分区数)
  2. 数据不丢失的前提下,将分区表的m个分区拆分为n个分区(m<n),即增量重新组织分区
    • RANGE:ALTER TABLE tb REORGANIZE PARTITION p0,p1,... INTO ( range_partitions_exprs(n) )
    • LIST:ALTER TABLE tb REORGANIZE PARTITION p0,p1,... INTO ( list_partitions_exprs(n) )

不能使用REORGANIZE PARTITION来改变表的分区类型;也就是说,例如,不能把RANGE分区变为HASH分区,反之亦然。也不能使用该命令来改变分区表达式或列。

  1. 重建分区,即先删除分区中的所有记录,然后重新插入。可用于整理分区碎片。
    • ALTER TABLE tb REBUILD PARTITION p0, p1;
  2. 优化分区,整理分区碎片
    • ALTER TABLE tb OPTIMIZE PARTITION p0, p1;

如从分区中删除了大量的行,或者对一个带有可变长度字段(VARCHAR、BLOB、TEXT类型)的行作了许多修改,可以使用优化分区来收回没有使用的空间,并整理分区数据文件的碎片。
5. 修复分区,修补被破坏的分区。
- ALTER TABLE tb REPAIR PARTITION p0,p1;
6. 检查分区,这个命令可以告诉你分区中的数据或索引是否已经被破坏,如果被破坏,请使用修复分区来修补
- ALTER TABLE tb CHECK PARTITION p1;

1.2.3 删除分区

  1. 删除一个分区,以及分区内的所有数据:
    • ALTER TABLE tb DROP PARTITION p2;
  2. 删除一个分区,但保留分区内的所有数据(MySQL 5.5引入):
    • ALTER TABLE tb TRUNCATE PARTITION p2;

1.2.4 查询分区数据

  1. 查看某个schema下某个表的分区信息
    • SELECT * FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA = 'xxx' AND TABLE_NAME LIKE 'xxxx';
  2. 分析某个分区,主要看行数和名称以及状态
    • ALTER TABLE tb ANALYZE PARTITION p3;

2 分表

分表顾名思义,就是把一张超大的数据表,拆分为多个较小的表,使得一些超大表的痼疾,得到有效的缓解。

超大表会带来如下的影响:

  1. 单表数据量太大,会被频繁读写,加锁操作密集,导致性能降低。
  2. 单表数据量太大,对应的索引也会很大,查询效率降低,增删操作的性能也会降低。

分表和分区看起来十分类似,确实,分区已经能够在磁盘层面将一张表拆分成多个文件了,理论上前面提到的大表的问题都能得到有效解决。因为分区就是分表的数据库实现版本。

在MySQL 5.1分区功能出现以前,要想解决超大表问题,只能采用分表操作,因为这类问题十分常见,MySQL才自带了一个分区功能,以达到相同的效果。

所以你可以直接说分区就是分表的替代,分表是分区出现以前的做法。不过这不代表我们就没有必要学习分表了,相反,水平分表的功能或许可以用更加便捷的分区来替代,但是垂直分表的功能,分区却无法替代。

分表只能通过程序代码来实现,目前市面上有许多分表的框架。

2.1 分表和分区的区别

  1. 分区只是一张表中的数据和索引的存储位置发生改变,分表则是将一张表分成多张表,是真实的有多套表的配套文件
  2. 分区没法突破数据库层面,不论怎么分区,这些分区都要在一个数据库下。而分表可以将子表分配在同一个库中,也可以分配在不同库中,突破数据库性能的限制。
  3. 分区只能替代水平分表的功能,无法取代垂直分表的功能。

2.2 分表的类型

分表分为水平分表和垂直分表。

2.2.1 水平分表

水平分表和分区很像,或者说分区就是水平分表的数据库实现版本,它们分的都是行记录,就像用一把刀,水平的将一个表切成多张表一样。

针对数据量巨大的单张表(比如订单表),我们按照某种规则,切分到多张表里面去。

但是需要注意,如果这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。分表可以将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与子表,这是分区所不能有的优势。

水平分表的切分规则一般有如下几种:

  1. 范围切分
    • 可以根据某个字段的范围做划分,比如订单号字段,从0到10000一个表,10001到20000一个表。
  2. HASH取模
    • 可以根据某个字段的HASH取模做划分,比如将一个用户表分成10个子表,可以取用户id,然后hash后取10的模,从而分配到不同的数据库上。不过这种划分一旦确定后,就无法改变子表数量了。
  3. 地理/国籍/类型等
    • 比如按照华东,华南,华北这样来区分业务表,或者安卓用户,IOS用户等来区分用户表。
  4. 时间
    • 按照时间切分,比如将6个月前,甚至一年前的数据切出去放到另外的一张表,因为随着时间流逝,这些表的数据被查询的概率变小,所以没必要和“热数据”放在一起,这个也是“冷热数据分离”。

2.2.2 垂直分表

水平分表分的是行记录,而垂直分表,分的是列字段,它就像用一把刀,垂直的将一个表切成多张表一样。

垂直分表是基于列字段进行的。一般是表中的字段较多,或者有数据较大长度较长(比如text,blob,varchar(1000)以上的字段)的字段时,我们将不常用的,或者数据量大的字段拆分到“扩展表”上。这样避免查询时,数据量太大造成的“跨页”问题。

垂直分表的切分规则很好理解,一般是“不常用”或者“字段数据量大”这两点来做切割,我们不多赘述。

3 分库

分库同样是为了应对超大数据带来的巨大的IO需求,如果不拆库,那么单库所能支持的吞吐能力和磁盘空间,就会成为制衡业务发展的瓶颈。分库的主要目的是为突破单节点数据库服务器的I/O能力限制,解决数据库水平扩展性问题。

3.1 分区分表之外的分库作用

也许你会问,我们有了分区和分表技术,还需要分库来解决大数据量的问题吗?对的,需要。

分区和分表可以把单表分到不同的硬盘上,但不能分配到不同服务器上。一台机器的性能是有限制的,用分库可以解决单台服务器性能不够,或者成本过高问题。

将一个库分成多个库,并在多个服务器上部署,就可以突破单服务器的性能瓶颈,这是分库必要性的最主要原因。

3.2 分库的类型

分库同样分为水平分库和垂直分库。

  1. 水平分库
    • 水平分库和水平分表相似,并且关系紧密,水平分库就是将单个库中的表作水平分表,然后将子表分别置于不同的子库当中,独立部署。
    • 因为库中内容的主要载体是表,所以水平分库和水平分表基本上如影随形。
    • 例如用户表,我们可以使用注册时间的范围来分表,将2020年注册的用户表usrtb2020部署在usrdata20中,2021年注册的用户表usrtb2021部署在usrdata21中。
  2. 垂直分库
    • 同样的,垂直分库和垂直分表也十分类似,不过垂直分表拆分的是字段,而垂直分库,拆分的是表。
    • 垂直分库是将一个库下的表作不同维度的分类,然后将其分配给不同子库的策略。
    • 例如,我们可以将用户相关的表都放置在usrdata这个库中,将订单相关的表都放置在odrdata中,以此类推。
    • 垂直分库的分类维度有很多,可以按照业务模块划分(用户/订单…),按照技术模块分(日志类库/图片类库…),或者空间,时间等等。

4 分库分表存在的问题

  1. 事务问题。
    • 问题描述:在执行分库分表之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。
    • 解决方法:利用分布式事务,协调不同库之间的数据原子性,一致性。
  1. 跨库跨表的join问题。

    • 问题描述:在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上,这时,表的关联操作将受到限制,我们无法join位于不同分库的表,也无法join分表粒度不同的表,结果原本一次查询能够完成的业务,可能需要多次查询才能完成。
    • 解决方法:tddl、MyCAT等都支持跨分片join。但是我们应该尽力避免跨库join,如果一定要整合数据,那么请在代码中多次查询完成。
  2. 额外的数据管理负担和数据运算压力。

    • 问题描述:额外的数据管理负担,最显而易见的就是数据的定位问题和数据的增删改查的重复执行问题,这些都可以通过应用程序解决,但必然引起额外的逻辑运算,例如,对于一个记录用户成绩的用户数据表userTable,业务要求查出成绩最好的100位,在进行分表之前,只需一个order by语句就可以搞定,但是在进行分表之后,将需要n个order by语句,分别查出每一个分表的前100名用户数据,然后再对这些数据进行合并计算,才能得出结果。
    • 解决方法:无解,这是水平拓展的代价。

5 分库分表方案产品

目前市面上的分库分表中间件相对较多,其中基于代理方式的有MySQL Proxy和Amoeba;基于Hibernate框架的是Hibernate Shards;基于jdbc的有当当sharding-jdbc;基于mybatis的类似maven插件式的有蘑菇街的蘑菇街TSharding;通过重写spring的ibatis template类的Cobar Client。

还有一些大公司的开源产品:

0%