分库分表
分库分表
垂直拆分
对于一个刚上线的互联网项目来说,由于前期活跃用户数量并不多,并发量也相对较小,所以此时企业一般都会选择将所有数据存放在一个数据库 中进行访问操作。举例来说,对于一个电商系统,其用户模块和产品模块的表刚开始都是位于一个库中。
user、
这里的垂直拆分,指的是将一个包含了很多表的数据库,根据表的功能的不同,拆分为多个小的数据库,每个库包含部分表。通常来说,垂直拆分,都是根据业务来对一个库中的表进行拆分的。关于垂直拆分,还有另一种说法,将一个包含了很多字段的大表拆分为多个小表,每个表包含部分字段,这种情况在实际开发中基本很少遇到。
垂直拆分的另一个典型应用场景是服务化
Sharding | 水平分区
经过水平分区设置后的业务表,必然能够将原本一张表维护的海量数据分配给
-
只分表:将
db 库中的user 表拆分为2 个分表,user_0 和user_1 ,这两个表还位于同一个库中。适用场景:如果库中的多个表中只有某张表或者少数表数据量过大,那么只需要针对这些表进行拆分,其他表保持不变。 -
只分库:将
db 库拆分为db_0 和db_1 两个库,同时在db_0 和db_1 库中各自新建一个user 表,db_0.user 表和db_1.user 表中各自只存原来的db.user 表中的部分数据。 -
分库分表:将
db 库拆分为db_0 和db_1 两个库,db_0 中包含user_0 、user_1 两个分表,db_1 中包含user_2 、user_3 两个分表。下图演示了在分库分表的情况下,数据是如何拆分的:假设db 库的user 表中原来有4000W 条数据,现在将db 库拆分为2 个分库db_0 和db_1 ,user 表拆分为user_0 、user_1、user_2、user_3 四个分表,每个分表存储1000W 条数据。
水平分区的优势
-
存储能力的水平扩展:在读写分离的情况下,每个集群中的
master 和slave 基本上数据是完全一致的,从存储能力来说,在存在海量数据的情况下,可能由于磁盘空间的限制,无法存储所有的数据。而在分库分表的情况下,我们可以搭建多个mysql 主从复制集群,每个集群只存储部分分片的数据,实现存储能力的水平扩展。 -
写能力的水平扩展:在读写分离的情况下,由于每个集群只有一个
master ,所有的写操作压力都集中在这一个节点上,在写入并发非常高的情况下,这里会成为整个系统的瓶颈。而在分库分表的情况下,每个分片所属的集群都有一个master 节点,都可以执行写入操作,实现写能力的水平扩展。此外减小建立索引开销,降低写操作的锁操作耗时等,都会带来很多显然的好处。
SQL 解析与执行
分库分表的挑战主要体现在
对于开发人员而言,虽然分库分表的,但是其还是希望能和单库单表那样的去操作数据库。例如我们要批量插入四条用户记录,并且希望根据用户的
insert into user(id,name) values (1,”tianshouzhi”),(2,”huhuamin”), (3,”wanghanao”),(4,”luyang”)

这种
insert into user0(id,name) values (4,”luyang”)
...
该操作包含以下流程:
-
sql 解析:首先对sql 进行解析,得到需要插入的四条记录的id 字段的值分别为1,2,3,4 -
sql 路由:sql 路由包括库路由和表路由。库路由用于确定这条记录应该插入哪个库,表路由用于确定这条记录应该插入哪个表。 -
sql 改写:因为一条记录只能插入到一个库中,而上述批量插入的语法将会在 每个库中都插入四条记录,明显是不合适的,因此需要对sql 进行改写,每个库只插入一条记录。 -
sql 执行:一条sql 经过改写后变成了多条sql ,为了提升效率应该并发的到不同的库上去执行,而不是按照顺序逐一执行 -
结果集合并:每个
sql 执行之后,都会有一个执行结果,我们需要对分库分表的结果集进行合并,从而得到一个完整的结果。
SQL 解析
用户执行只是一条
通常来说,对于

例如
SQL 路由
路由规则是分库分表的基础,其规定了数据应该按照怎样的规则路由到不同的分库分表中。对于一个数据库中间件来说,通常是支持用户自定义任何路由规则的。路由规则本质上是一个脚本表达式,数据库中间件通过内置的脚本引擎对表达式进行计算,确定最终要操作哪些分库、分表。
常见的路由规则包括哈希取模,按照日期等。常见的路由规则包括哈希取模,按照日期等。常见的用户表分表时,使用
不管执行的是
SQL 改写
通常对于
当然也有一些数据库中间件,不满足于只支持
SQL 执行
当经过

这些执行的
结果集合并
结果集合并,是数据库中间件的一大难点,需要针对性分析,主要是考虑实现的复杂度,以及执行的效率问题,对于一些复杂的
-
对于查询条件:大部分中间件都支持
= 、IN 作为查询条件,且可以作为分区字段。但是对于NIT IN 、BETWEEN…AND、LIKE,NOT LIKE 等,只能作为普通的查询条件,因为根据这些条件,无法记录到底是在哪个分库或者分表,只能全表扫描。 -
聚合函数:大部分中间件都支持
MAX 、MIN、COUNT、SUM,但是对于AVG 可能只是部分支持。另外,如果是函数嵌套、分组(GROUP BY) 聚合,可能也有一些数据库中间件不支持。 -
子查询:分为
FROM 部分的子查询和WHERE 部分的子查询。大部分中对于子查询的支持都是非常有限,例如语法上兼容,但是无法识别子查询中的分区字段,或者要求子查询的表名必须与外部查询表名相同,又或者只能支持一级嵌套子查询。 -
JOIN:对于
JOIN 的支持通常很复杂,如果做不到过滤条件下推和流式读取,在中间件层面,基本无法对JOIN 进行支持,因为不可能把两个表的所有分表,全部拿到内存中来进行JOIN ,内存早就崩了。 -
分页排序:通常中间件都是支持
ORDER BY 和LIMIT 的。但是在分库分表的情况下,分页的效率较低。例如对于limit 100,10 ORDER BY id
。表示按照id 排序,从第100 个位置开始取10 条记录。那么,大部分数据库中间件实际上是要从每个分表都查询110(100+10) 条记录,拿到内存中进行重新排序,然后取出10 条。假设有10 个分表,那么实际上要查询1100 条记录,而最终只过滤出了10 记录。因此,在分页的情况下,通常建议使用where id > ? limit 10
的方式来进行查询,应用记住每次查询的最大的记录id 。之后查询时,每个分表只需要从这个id 之后,取10 条记录即可,而不是取offset + rows 条记录。
Binding Table
小表广播
分布式系统
在分库分表后,我们不能再使用
分布式事务是分库分表绕不过去的一个坎,因为涉及到了同时更新多个分片数据。例如上面的批量插入记录到四个不同的库,如何保证要么同时成功,要么同时失败。关于分布式事务,
动态扩容
动态扩容指的是增加分库分表的数量。例如原来的