数据拆分的三种方案
当数据库的数据量变得特别大,影响到查询和更新效率的时候,我们就得考虑做数据拆分了。数据拆分一般包含三种方式:分区,分表和分库,我们先分别来讲一讲:
分区:数据分区是数据库提供的一种表结构设计方式,对一张数据表进行分区并不会创建新的表, 只是将原本存储为单个文件的表数据根据一定的规则拆分为多个文件进行存储(不同的文件还可以放到不同的硬盘上),这样可以有效降低单个文件的IO压力,提高读写效率,部分聚合语句也可以在不同的分区上并行的执行,缩短执行时间。目前主流数据库都支持数据分区技术,这个活儿一般都是DBA直接做的,和应用层关系不大;
分表:分表就是将原本的一张数据表拆分成多张表,比如t_order拆成t_order_0,t_order_1,数据按照不同规则存储到不同的表中。分表具备分区的所有好处(表都分了自然数据文件也分成多个了,IO性能也提高了),同时还能够提高单表的并发访问性能(请求会根据规则路由到不同的分表中并发执行);
分库:分库就更进一步了,将业务数据库直接拆分成多个,数据库请求会根据路由规则访问不同的分库。分库可以部署到不同的服务器上,从而打破单机的硬件和网络瓶颈,大大提高数据库的整体并发能力,是数据库水平扩展的重要方式。
由此看来分区,分库,分表是层层递进的关系,能够带来的好处也是越来越多。但便宜也不是白捡的,三种拆分方式实现起来也是越往后越困难。分区最简单,刚才说了,这东西数据库一般原生支持,DBA几个命令就能搞定;分表和分库除了数据库层面需要维护更多的表和库以外,都还需要对应用层进行改造,改造的难度根据所选用的方式有所不同。另外相对于分表,分库还需要面对事务支持的问题,分表只是在单个数据库中进行拆分,不会对本地事务造成影响。但分库以后,应用程序需要同时连接多个数据源进行操作,跨数据源的事务只能使用分布式事务的解决方案,而分布式事务本身又是一块难啃的骨头。
我们在设计数据拆分方案的时候也要有所取舍,必须想清楚拆分是为了解决哪些具体问题。一般来说,如果只是因为单表数据量比较大影响查询更新的效率,但系统并发量访问不高的情况,直接分区就可以了;如果数据和并发量都比较大,就需要考虑分表了;如果单节点的数据库都已经撑不住了,比如存储空间,网络带宽,并发数(单库能承载的并发数一般也就1,2K)等,那就只有分库了。能走到分库这一步,说明你们公司业务发展的不错呀,涨工资指日可待啦<( ̄︶ ̄)>
分库分表的方案选择
啰嗦了这么多,我们进入这次的正题,怎么使用sharding-jdbc去做分表分库呢?前面说过,分库分表数据库是不管的,应用程序需要自行去处理业务数据与各个分表和分库的映射关系,比如客户A的订单要保存到表order_a,而客户B的订单需要保存到表order_b,如果全部让程序员手工去写处理逻辑,那估计得原地爆炸吧...... 好在,计算机的问题几乎都可以通过添加一个中间抽象层来解决问题,JDBC不就是通过抽象和统一接口来解决了java程序与不同的数据库进行通讯的问题吗?那只要再在JDBC之上再做一层抽象,由这个抽象层来处理业务数据到分库分表的路由问题,程序员不就能解放了吗。sharding-jdbc就是一个提供这层抽象的框架,sharding-jdbc最早是由当当开源出来的,现在已经划入 ShardingSphere 项目成为了Apache的顶级项目,ShardingSphere目前包括三个子项目:Sharding-JDBC,Sharding-Proxy,Sharding-Sidecar,都是用于处理分库分表的解决方案,不过方式有所不同,Sharding-JDBC是以框架的形式直接集成到业务代码中,而其它两个都是以中间件的形式独立部署的,具体的差异见下表:
Sharding-Sidecar看起来无疑是最好的选择,其模仿了微服务领域的Service Mesh的概念,提出了所谓的Database Mesh,但目前该架构还不成熟,仅仅是Alpha状态。Sharding-Proxy需要引入和维护额外的中间件,其类似的框架还有 Mycat,而且目前只支持Mysql数据库的代理。所以,对于分库和应用实例都比较少的Java项目,我个人觉得Sharding-JDBC应该是目前最好的解决方案了。
接入Sharding-JDBC
首先我们先来初始化数据库的表结构,逻辑表有两张:t_order(订单表)和t_order_good(订单产品表,用于存储订单中包含的产品明细),每张表划分为三张分表,一共六张表:t_order_0,t_order_1,t_order_2,t_order_good_1,t_order_good_2,t_order_good_3,建表语句如下:
CREATE DATABASE `sharding-db-1` /*!40100 DEFAULT CHARACTER SET utf8 */;
CREATE TABLE `t_order_0` (
`id` bigint(20) NOT NULL,
`order_code` varchar(32) DEFAULT NULL COMMENT '订单编号',
`product_line` varchar(12) DEFAULT NULL,
`customer_code` varchar(32) DEFAULT NULL COMMENT '客户ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order_1` (
`id` bigint(20) NOT NULL,
`order_code` varchar(32) DEFAULT NULL COMMENT '订单编号',
`product_line` varchar(12) DEFAULT NULL,
`customer_code` varchar(32) DEFAULT NULL COMMENT '客户ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order_2` (
`id` bigint(20) NOT NULL,
`order_code` varchar(32) DEFAULT NULL COMMENT '订单编号',
`product_line` varchar(12) DEFAULT NULL,
`customer_code` varchar(32) DEFAULT NULL COMMENT '客户ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order_good_1` (
`id` bigint(20) NOT NULL,
`order_id` bigint(20) DEFAULT NULL,
`good_code` varchar(32) DEFAULT NULL,
`good_type` varchar(32) DEFAULT NULL,
`good_spec` varchar(32) DEFAULT NULL,
`good_quantity` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order_good_2` (
`id` bigint(20) NOT NULL,
`order_id` bigint(20) DEFAULT NULL,
`good_code` varchar(32) DEFAULT NULL,
`good_type` varchar(32) DEFAULT NULL,
`good_spec` varchar(32) DEFAULT NULL,
`good_quantity` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order_good_3` (
`id` bigint(20) NOT NULL,
`order_id` bigint(20) DEFAULT NULL,
`good_code` varchar(32) DEFAULT NULL,
`good_type` varchar(32) DEFAULT NULL,
`good_spec` varchar(32) DEFAULT NULL,
`good_quantity` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
然后新建一个Spring Boot项目,接入Sharding-JDBC非常简单,按老规矩引入相应的Starter就行了,这里同时引入Mybatis-Plus框架便于处理各种数据库操作:
com.baomidou
mybatis-plus-boot-starter
3.3.0
org.apache.shardingsphere
sharding-jdbc-spring-boot-starter
4.1.0
其实Sharding-JDBC只是对datasource进行封装,所以上层的持久层框架一般不会受到什么影响,愿意用什么框架都可以。加入一个application-sharding.yml配置文件,专门用于分库分表的相关配置(加到主配置文件也可以,但分库分表的配置项较多,建议拆分出来独立配置):
#sharding-jdbc配置
spring:
shardingsphere:
props:
sql:
show: true #是否显示分片后实际的执行SQL
datasource:
names: ds0
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.211:3306/sharding-db-1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password: *******
sharding:
# 配置绑定表,每一行为一组
binding-tables:
- t_order,t_order_good
tables:
t_order:
actual-data-nodes: ds$->{0}.t_order_$->{0..2}
table-strategy:
inline:
sharding-column: id
algorithm-expression: t_order_$->{id % 3}
key-generator:
column: id
type: SNOWFLAKE
t_order_good:
actual-data-nodes: ds$->{0}.t_order_good_$->{0..2}
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_good_$->{order_id % 3}
key-generator:
column: id
type: SNOWFLAKE
这些配置项应该算是整个接入过程中最复杂的地方,我们来逐项说一下:
- spring.shardingsphere.datasource下面用于配置所有需要接入的数据源,一般一个数据源就是一个独立的数据库,我们先只配置一个数据库来测试一下单库分表的实现;
- spring.shardingsphere.sharding下面就是分片规则的配置了,binding-tables是个很重要的概念,指分片规则一致的主表和子表,比如t_order和t_order_good表,都是按照order_id来进行分片的,则它们之间就是绑定关系,绑定表之间的多表关联查询会被内部优化,从而避免出现笛卡尔积式的关联,使得查询效率有极大的提升,详细介绍可以参考 这里。
- tables下面可以分别为不同的表配置不同的分片规则,actualDataNodes表示该逻辑表对应的真实表的实际位置,比如t_order的真实表包括数据源ds0下的t_order_0,t_order_1,t_order_2;tableStrategy.inline表示使用InlineShardingStrategyConfiguration,可以在配置中使用行表达式,实际就是一段Groovy代码,具体语法规则参考 这里,需要注意的是InlineShardingStrategy只支持单分片键,且仅支持SQL语句中的=和IN的分片操作。如果需要将时间等支持范围查询的字段作为分片键,那就需要使用其它的分片策略,比如StandardShardingStrategy。shardingColumn代表分片键的名称,algorithmExpression是分片策略的表达式,这里简单的采用对mod(order_id)的方式进行分片;keyGenerator用于生成分布式主键,这里采用的是SNOWFLAKE算法,可以生成一个可排序的Long型ID值,最大只有 19位,比UUID好多了。
配置完成以后,基本已经大功告成了,剩下的就是写一些数据库插入查询的样本代码,这里就不贴了。然后我们来测试一下分片的成果,首先是数据插入:
@Test
public void testCreateOrder() throws Exception {
List productLines = ListUtil.toList("TC", "UB");
List newOrders = new ArrayList<>();
List newOrderGoods = new ArrayList<>();
for (int i = 0; i < 30; i++) {
Order order = new Order();
order.setOrderCode(System.currentTimeMillis() + RandomUtil.randomString(6));
order.setCreateTime(new Date());
order.setProductLine(productLines.get(RandomUtil.randomInt(0, 2)));
newOrders.add(order);
}
//插入订单数据
orderService.saveBatch(newOrders);
for (Order order : newOrders) {
OrderGood aGood = new OrderGood();
aGood.setOrderId(order.getId());
aGood.setGoodCode(RandomUtil.randomString(6));
aGood.setGoodQuantity(RandomUtil.randomInt(1, 200));
newOrderGoods.add(aGood);
}
//插入订单产品数据
orderGoodService.saveBatch(newOrderGoods);
}
执行之后,查询各表的数据,数据已经正确插入到各个分表当中了。需要注意的是,snowflake算法生成的ID并不是完全连续的,所有导致取模操作后并没有完全平均的插入到各个分表中,但总体上相差不多。然后我们再来执行一下表的连接查询,查询SQL如下:
SELECT
t.*, t1.order_code
FROM
t_order_good t
inner join
t_order t1 ON t.order_id = t1.id
执行时,我们把spring.shardingsphere.props.sql.show设置为true,便于观察解析后实际执行的SQL情况,执行后实际打印的SQL语句如下:
SELECT t.*, t1.order_code FROM t_order_good_0 t inner join t_order_0 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_1 t inner join t_order_1 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_2 t inner join t_order_2 t1 ON t.order_id = t1.id;
这个结果是符合我们的预期的,因为并没有传入分片key作为where条件,所以连接时会尝试按照主表与从表间的分片按顺序进行一一对应进行join(和分表名称无关,只和顺序相关,1-A,2-B,3-C这样连接也是可以的,所以主从表的分片策略最好是设置为完全一致,包括分表的数量,否则如果没有指定where条件就无法进行对应了)。如果我们此时将之前设置的bindingTables去掉,就会发现,执行的SQL由三个变成了9个,不再是按顺序连接了,而是笛卡尔积:
SELECT t.*, t1.order_code FROM t_order_good_0 t inner join t_order_0 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_0 t inner join t_order_1 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_0 t inner join t_order_2 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_1 t inner join t_order_0 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_1 t inner join t_order_1 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_1 t inner join t_order_2 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_2 t inner join t_order_0 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_2 t inner join t_order_1 t1 ON t.order_id = t1.id;
SELECT t.*, t1.order_code FROM t_order_good_2 t inner join t_order_2 t1 ON t.order_id = t1.id;
分表看起来应该没什么问题了,至于分库其实也非常简单,只需要根据情况多配置几个数据源,然后在actualDataNodes中指定对应的数据源范围,再配置一下database的路由规则就行了,比如如果需要按照订单的产品线进行分库(TC,UB两个产品线),那我们的配置文件可修改为:
#sharding-jdbc配置
spring:
shardingsphere:
props:
sql:
show: true #是否显示分片后实际的执行SQL
datasource:
names: ds_TC,ds_UB
ds_TC:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.211:3306/sharding-db-1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password:
ds_UB:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.211:3306/sharding-db-1?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password:
sharding:
# 配置绑定表,每一行为一组
binding-tables:
- t_order,t_order_good
tables:
t_order:
actual-data-nodes: ds_$->{[TC,UB]}.t_order_$->{0..2}
table-strategy:
inline:
sharding-column: id
algorithm-expression: t_order_$->{id % 3}
key-generator:
column: id
type: SNOWFLAKE
t_order_good:
actual-data-nodes: ds_$->{[TC,UB]}.t_order_good_$->{0..2}
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_good_$->{order_id % 3}
key-generator:
column: id
type: SNOWFLAKE
# 默认的分库规则,适用于所有表,按产品线进行分库
default-database:
strategy:
inline:
sharding-column: product_line
algorithm-expression: ds_$->{product_line}
本文的所有代码可以在这里找到:sharding-jdbc-demo