Sharding-JDBC
最早是当当网内部使用的一款分库分表框架,到2017年的时候才开始对外开源,这几年在大量社区贡献者的不断迭代下,功能也逐渐完善,现已更名为 ShardingSphere
,2020年4⽉16⽇正式成为 Apache
软件基⾦会的顶级项⽬。
随着版本的不断更迭 ShardingSphere
的核心功能也变得多元化起来。从最开始 Sharding-JDBC 1.0 版本只有数据分片,到 Sharding-JDBC 2.0 版本开始支持数据库治理(注册中心、配置中心等等),再到 Sharding-JDBC 3.0版本又加分布式事务 (支持 Atomikos
、Narayana
、Bitronix
、Seata
),如今已经迭代到了 Sharding-JDBC 4.0 版本。
在这里插入图片描述
现在的 ShardingSphere 不单单是指某个框架而是一个生态圈,这个生态圈 Sharding-JDBC
、Sharding-Proxy
和 Sharding-Sidecar
这三款开源的分布式数据库中间件解决方案所构成。
ShardingSphere
的前身就是 Sharding-JDBC
,所以它是整个框架中最为经典、成熟的组件,我们先从 Sharding-JDBC
框架入手学习分库分表。
在开始 Sharding-JDBC
分库分表具体实战之前,我们有必要先了解分库分表的一些核心概念。
一般我们在提到分库分表的时候,大多是以水平切分模式(水平分库、分表)为基础来说的,数据分片将原本一张数据量较大的表 t_order
拆分生成数个表结构完全一致的小数据量表 t_order_0
、t_order_1
、···、t_order_n
,每张表只存储原大表中的一部分数据,当执行一条SQL
时会通过 分库策略
、分片策略
将数据分散到不同的数据库、表内。
在这里插入图片描述
数据节点是分库分表中一个不可再分的最小数据单元(表),它由数据源名称和数据表组成,例如上图中 order_db_1.t_order_0
、order_db_2.t_order_1
就表示一个数据节点。
逻辑表是指一组具有相同逻辑和数据结构表的总称。比如我们将订单表t_order
拆分成 t_order_0
··· t_order_9
等 10张表。此时我们会发现分库分表以后数据库中已不在有 t_order
这张表,取而代之的是 t_order_n
,但我们在代码中写 SQL
依然按 t_order
来写。此时 t_order
就是这些拆分表的逻辑表
。
真实表也就是上边提到的 t_order_n
数据库中真实存在的物理表。
用于分片的数据库字段。我们将 t_order
表分片以后,当执行一条SQL时,通过对字段 order_id
取模的方式来决定,这条数据该在哪个数据库中的哪个表中执行,此时 order_id
字段就是 t_order
表的分片健。
在这里插入图片描述
这样以来同一个订单的相关数据就会存在同一个数据库表中,大幅提升数据检索的性能,不仅如此 sharding-jdbc
还支持根据多个字段作为分片健进行分片。
上边我们提到可以用分片健取模的规则分片,但这只是比较简单的一种,在实际开发中我们还希望用 >=
、<=
、>
、<
、BETWEEN
和 IN
等条件作为分片规则,自定义分片逻辑,这时就需要用到分片策略与分片算法。
从执行 SQL 的角度来看,分库分表可以看作是一种路由机制,把 SQL 语句路由到我们期望的数据库或数据表中并获取数据,分片算法可以理解成一种路由规则。
咱们先捋一下它们之间的关系,分片策略只是抽象出的概念,它是由分片算法和分片健组合而成,分片算法做具体的数据分片逻辑。
分库、分表的分片策略配置是相对独立的,可以各自使用不同的策略与算法,每种策略中可以是多个分片算法的组合,每个分片算法可以对多个分片健做逻辑判断。
分片算法和分片策略的关系
注意:sharding-jdbc 并没有直接提供分片算法的实现,需要开发者根据业务自行实现。
sharding-jdbc
提供了4种分片算法:
1、精确分片算法
精确分片算法(PreciseShardingAlgorithm)用于单个字段作为分片键,SQL中有 =
与 IN
等条件的分片,需要在标准分片策略(StandardShardingStrategy
)下使用。
2、范围分片算法
范围分片算法(RangeShardingAlgorithm)用于单个字段作为分片键,SQL中有 BETWEEN AND
、>
、<
、>=
、<=
等条件的分片,需要在标准分片策略(StandardShardingStrategy
)下使用。
3、复合分片算法
复合分片算法(ComplexKeysShardingAlgorithm)用于多个字段作为分片键的分片操作,同时获取到多个分片健的值,根据多个字段处理业务逻辑。需要在复合分片策略(ComplexShardingStrategy
)下使用。
4、Hint分片算法
Hint分片算法(HintShardingAlgorithm)稍有不同,上边的算法中我们都是解析SQL
语句提取分片键,并设置分片策略进行分片。但有些时候我们并没有使用任何的分片键和分片策略,可还想将 SQL 路由到目标数据库和表,就需要通过手动干预指定SQL的目标数据库和表信息,这也叫强制路由。
上边讲分片算法的时候已经说过,分片策略是一种抽象的概念,实际分片操作的是由分片算法和分片健来完成的。
1、标准分片策略
标准分片策略适用于单分片键,此策略支持 PreciseShardingAlgorithm
和 RangeShardingAlgorithm
两个分片算法。
其中 PreciseShardingAlgorithm
是必选的,用于处理 =
和 IN
的分片。RangeShardingAlgorithm
是可选的,用于处理BETWEEN AND
, >
, <
,>=
,<=
条件分片,如果不配置RangeShardingAlgorithm
,SQL中的条件等将按照全库路由处理。
2、复合分片策略
复合分片策略,同样支持对 SQL语句中的 =
,>
, <
, >=
, <=
,IN
和 BETWEEN AND
的分片操作。不同的是它支持多分片键,具体分配片细节完全由应用开发者实现。
3、行表达式分片策略
行表达式分片策略,支持对 SQL语句中的 =
和 IN
的分片操作,但只支持单分片键。这种策略通常用于简单的分片,不需要自定义分片算法,可以直接在配置文件中接着写规则。
t_order_$->{t_order_id % 4}
代表 t_order
对其字段 t_order_id
取模,拆分成4张表,而表名分别是t_order_0
到 t_order_3
。
4、Hint分片策略
Hint分片策略,对应上边的Hint分片算法,通过指定分片健而非从 SQL
中提取分片健的方式进行分片的策略。
数据分⽚后,不同数据节点⽣成全局唯⼀主键是⾮常棘⼿的问题,同⼀个逻辑表(t_order
)内的不同真实表(t_order_n
)之间的⾃增键由于⽆法互相感知而产⽣重复主键。
尽管可通过设置⾃增主键 初始值
和 步⻓
的⽅式避免ID碰撞,但这样会使维护成本加大,乏完整性和可扩展性。如果后去需要增加分片表的数量,要逐一修改分片表的步长,运维成本非常高,所以不建议这种方式。
实现分布式主键⽣成器的方式很多,可以参考我之前写的《9种分布式ID生成方式》。
为了让上手更加简单,ApacheShardingSphere 内置了UUID
、SNOWFLAKE
两种分布式主键⽣成器,默认使⽤雪花算法(snowflake
)⽣成64bit的⻓整型数据。不仅如此它还抽离出分布式主键⽣成器的接口,⽅便我们实现⾃定义的⾃增主键⽣成算法。
广播表:存在于所有的分片数据源中的表,表结构和表中的数据在每个数据库中均完全一致。一般是为字典表或者配置表 t_config
,某个表一旦被配置为广播表,只要修改某个数据库的广播表,所有数据源中广播表的数据都会跟着同步。
绑定表:那些分片规则一致的主表和子表。比如:t_order
订单表和 t_order_item
订单服务项目表,都是按 order_id
字段分片,因此两张表互为绑定表关系。
那绑定表存在的意义是啥呢?
通常在我们的业务中都会使用 t_order
和 t_order_item
等表进行多表联合查询,但由于分库分表以后这些表被拆分成N多个子表。如果不配置绑定表关系,会出现笛卡尔积关联查询,将产生如下四条SQL
。
SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id
SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id
SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id
笛卡尔积查询
而配置绑定表关系后再进行关联查询时,只要对应表分片规则一致产生的数据就会落到同一个库中,那么只需 t_order_0
和 t_order_item_0
表关联即可。
SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id
绑定表关系
注意:在关联查询时
t_order
它作为整个联合查询的主表。所有相关的路由计算都只使用主表的策略,t_order_item
表的分片相关的计算也会使用t_order
的条件,所以要保证绑定表之间的分片键要完全相同。
那 Sharding-JDBC
拓展 JDBC API 接口后,在新增的分片功能里又做了哪些事情呢?
一张表经过分库分表后被拆分成多个子表,并分散到不同的数据库中,在不修改原业务 SQL 的前提下,Sharding-JDBC
就必须对 SQL进行一些改造才能正常执行。
大致的执行流程:SQL 解析
-> 执⾏器优化
-> SQL 路由
-> SQL 改写
-> SQL 执⾏
-> 结果归并
六步组成,一起瞅瞅每个步骤做了点什么。
在这里插入图片描述
SQL解析过程分为词法解析和语法解析两步,比如下边这条查询用户订单的SQL,先用词法解析将SQL拆解成不可再分的原子单元。在根据不同数据库方言所提供的字典,将这些单元归类为关键字,表达式,变量或者操作符等类型。
SELECT order_no,price FROM t_order_ where user_id = 10086 and order_status > 0
接着语法解析会将拆分后的SQL转换为抽象语法树,通过对抽象语法树遍历,提炼出分片所需的上下文,上下文包含查询字段信息(Field
)、表信息(Table
)、查询条件(Condition
)、排序信息(Order By
)、分组信息(Group By
)以及分页信息(Limit
)等,并标记出 SQL中有可能需要改写的位置。
抽象语法树
执⾏器优化对SQL分片条件进行优化,处理像关键字 OR
这种影响性能的坏味道。
SQL 路由通过解析分片上下文,匹配到用户配置的分片策略,并生成路由路径。简单点理解就是可以根据我们配置的分片策略计算出 SQL该在哪个库的哪个表中执行,而SQL路由又根据有无分片健区分出 分片路由
和 广播路由
。
官方路由图谱
有分⽚键的路由叫分片路由,细分为直接路由、标准路由和笛卡尔积路由这3种类型。
标准路由
标准路由是最推荐也是最为常⽤的分⽚⽅式,它的适⽤范围是不包含关联查询或仅包含绑定表之间关联查询的SQL。
当 SQL分片健的运算符为 =
时,路由结果将落⼊单库(表),当分⽚运算符是BETWEEN
或IN
等范围时,路由结果则不⼀定落⼊唯⼀的库(表),因此⼀条逻辑SQL最终可能被拆分为多条⽤于执⾏的真实SQL。
SELECT * FROM t_order where t_order_id in (1,2)
SQL路由处理后
SELECT * FROM t_order_0 where t_order_id in (1,2)
SELECT * FROM t_order_1 where t_order_id in (1,2)
直接路由
直接路由是通过使用 HintAPI
直接将 SQL路由到指定⾄库表的一种分⽚方式,而且直接路由可以⽤于分⽚键不在SQL中的场景,还可以执⾏包括⼦查询、⾃定义函数等复杂情况的任意SQL。
比如根据 t_order_id
字段为条件查询订单,此时希望在不修改SQL的前提下,加上 user_id
作为分片条件就可以使用直接路由。
笛卡尔积路由
笛卡尔路由是由⾮绑定表之间的关联查询产生的,查询性能较低尽量避免走此路由模式。
无分⽚键的路由又叫做广播路由,可以划分为全库表路由、全库路由、 全实例路由、单播路由和阻断路由这 5种类型。
全库表路由
全库表路由针对的是数据库 DQL
和 DML
,以及 DDL
等操作,当我们执行一条逻辑表 t_order
SQL时,在所有分片库中对应的真实表 t_order_0
··· t_order_n
内逐一执行。
全库路由
全库路由主要是对数据库层面的操作,比如数据库 SET
类型的数据库管理命令,以及 TCL 这样的事务控制语句。
对逻辑库设置 autocommit
属性后,所有对应的真实库中都执行该命令。
SET autocommit=0;
全实例路由
全实例路由是针对数据库实例的 DCL 操作(设置或更改数据库用户或角色权限),比如:创建一个用户 order ,这个命令将在所有的真实库实例中执行,以此确保 order 用户可以正常访问每一个数据库实例。
CREATE USER [email protected] identified BY '程序员内点事';
单播路由
单播路由用来获取某一真实表信息,比如获得表的描述信息:
DESCRIBE t_order;
t_order
的真实表是 t_order_0
···· t_order_n
,他们的描述结构相完全同,我们只需在任意的真实表执行一次就可以。
阻断路由
⽤来屏蔽SQL对数据库的操作,例如:
USE order_db;
这个命令不会在真实数据库中执⾏,因为 ShardingSphere
采⽤的是逻辑 Schema(数据库的组织和结构) ⽅式,所以无需将切换数据库的命令发送⾄真实数据库中。
将基于逻辑表开发的SQL改写成可以在真实数据库中可以正确执行的语句。比如查询 t_order
订单表,我们实际开发中 SQL是按逻辑表 t_order
写的。
SELECT * FROM t_order
但分库分表以后真实数据库中 t_order
表就不存在了,而是被拆分成多个子表 t_order_n
分散在不同的数据库内,还按原SQL执行显然是行不通的,这时需要将分表配置中的逻辑表名称改写为路由之后所获取的真实表名称。
SELECT * FROM t_order_n
将路由和改写后的真实 SQL 安全且高效发送到底层数据源执行。但这个过程并不是简单的将 SQL 通过JDBC 直接发送至数据源执行,而是平衡数据源连接创建以及内存占用所产生的消耗,它会自动化的平衡资源控制与执行效率。
将从各个数据节点获取的多数据结果集,合并成一个大的结果集并正确的返回至请求客户端,称为结果归并。而我们SQL中的排序、分组、分页和聚合等语法,均是在归并后的结果集上进行操作的。
下面我们结合 Springboot
+ mybatisplus
快速搭建一个分库分表案例。
先做准备工作,创建两个数据库 ds-0
、ds-1
,两个库中分别建表 t_order_0
、t_order_1
、t_order_2
、t_order_item_0
、t_order_item_1
、t_order_item_2
,t_config
,方便后边验证广播表、绑定表的场景。
t_order_0
订单表
CREATE TABLE `t_order_0` (
`order_id` bigint(200) NOT NULL,
`order_no` varchar(100) DEFAULT NULL,
`create_name` varchar(50) DEFAULT NULL,
`price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
t_order_0
与 t_order_item_0
互为关联表
CREATE TABLE `t_order_item_0` (
`item_id` bigint(100) NOT NULL,
`order_no` varchar(200) NOT NULL,
`item_name` varchar(50) DEFAULT NULL,
`price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
广播表 t_config
`id` bigint(30) NOT NULL,
`remark` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ShardingSphere
提供了4种分片配置方式:
Java 代码配置
Yaml 、properties 配置
Spring 命名空间配置
Spring Boot配置
为让代码看上去更简洁和直观,后边统一使用 properties
配置的方式,引入 shardingsphere
对应的 sharding-jdbc-spring-boot-starter
和 sharding-core-common
包,版本统一用的 4.0.0-RC1。
org.apache.shardingsphere
sharding-jdbc-spring-boot-starter
4.0.0-RC1
org.apache.shardingsphere
sharding-core-common
4.0.0-RC1
准备工作做完( mybatis 搭建就不赘述了),接下来我们逐一解读分片配置信息。
我们首先定义两个数据源 ds-0
、ds-1
,并分别加上数据源的基础信息。
# 定义两个全局数据源
spring.shardingsphere.datasource.names=ds-0,ds-1
# 配置数据源 ds-0
spring.shardingsphere.datasource.ds-0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds-0.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds-0.url=jdbc:mysql://127.0.0.1:3306/ds-0?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
spring.shardingsphere.datasource.ds-0.username=root
spring.shardingsphere.datasource.ds-0.password=root
# 配置数据源 ds-1
spring.shardingsphere.datasource.ds-1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds-1.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds-1.url=jdbc:mysql://127.0.0.1:3306/ds-1?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
spring.shardingsphere.datasource.ds-1.username=root
spring.shardingsphere.datasource.ds-1.password=root
配置完数据源接下来为表添加分库和分表策略,使用 sharding-jdbc
做分库分表需要我们为每一个表单独设置分片规则。
# 配置分片表 t_order
# 指定真实数据节点
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds-$->{0..1}.t_order_$->{0..2}
actual-data-nodes
属性指定分片的真实数据节点,$
是一个占位符,{0..1}表示实际拆分的数据库表数量。
ds-$->{0..1}.t_order_$->{0..2}
表达式相当于 6个数据节点
ds-0.t_order_0
ds-0.t_order_1
ds-0.t_order_2
ds-1.t_order_0
ds-1.t_order_1
ds-1.t_order_2
### 分库策略
# 分库分片健
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=order_id
# 分库分片算法
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds-$->{order_id % 2}
为表设置分库策略,上边讲了 sharding-jdbc
它提供了四种分片策略,为快速搭建我们先以最简单的行内表达式分片策略来实现,在下一篇会介绍四种分片策略的详细用法和使用场景。
database-strategy.inline.sharding-column
属性中 database-strategy
为分库策略,inline
为具体的分片策略,sharding-column
代表分片健。
database-strategy.inline.algorithm-expression
是当前策略下具体的分片算法,ds-$->{order_id % 2}
表达式意思是 对 order_id
字段进行取模分库,2 代表分片库的个数,不同的策略对应不同的算法,这里也可以是我们自定义的分片算法类。
# 分表策略
# 分表分片健
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id
# 分表算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{order_id % 3}
# 自增主键字段
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
# 自增主键ID 生成方案
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE
分表策略 和 分库策略 的配置比较相似,不同的是分表可以通过 key-generator.column
和 key-generator.type
设置自增主键以及指定自增主键的生成方案,目前内置了SNOWFLAKE
和 UUID
两种方式,还能自定义的主键生成算法类,后续会详细的讲解。
# 绑定表关系
spring.shardingsphere.sharding.binding-tables= t_order,t_order_item
必须按相同分片健进行分片的表才能互为成绑定表,在联合查询时就能避免出现笛卡尔积查询。
# 配置广播表
spring.shardingsphere.sharding.broadcast-tables=t_config
广播表,开启 SQL解析日志,能清晰的看到 SQL分片解析的过程
# 是否开启 SQL解析日志
spring.shardingsphere.props.sql.show=true
分片配置完以后我们无需在修改业务代码了,直接执行业务逻辑的增、删、改、查即可,接下来验证一下分片的效果。
我们同时向 t_order
、t_order_item
表插入 5条订单记录,并不给定主键 order_id
,item_id
字段值。
public String insertOrder() {
for (int i = 0; i < 4; i++) {
TOrder order = new TOrder();
order.setOrderNo("A000" + i);
order.setCreateName("订单 " + i);
order.setPrice(new BigDecimal("" + i));
orderRepository.insert(order);
TOrderItem orderItem = new TOrderItem();
orderItem.setOrderId(order.getOrderId());
orderItem.setOrderNo("A000" + i);
orderItem.setItemName("服务项目" + i);
orderItem.setPrice(new BigDecimal("" + i));
orderItemRepository.insert(orderItem);
}
return "success";
}
看到订单记录被成功分散到了不同的库表中, order_id
字段也自动生成了主键ID,基础的分片功能就完成了。
基础分片
那向广播表 t_config
中插入一条数据会是什么效果呢?
public String config() {
TConfig tConfig = new TConfig();
tConfig.setRemark("我是广播表");
tConfig.setCreateTime(new Date());
tConfig.setLastModifyTime(new Date());
configRepository.insert(tConfig);
return "success";
}
发现所有库中 t_config
表都执行了这条SQL,广播表和 MQ广播订阅的模式很相似,所有订阅的客户端都会收到同一条消息。
广播表
简单SQL操作验证没问通,接下来在试试复杂一点的联合查询,前边我们已经把 t_order
、t_order_item
表设为绑定表,直接联表查询执行一下。
关联查询
通过控制台日志发现,逻辑表SQL 经过解析以后,只对 t_order_0
和 t_order_item_0
表进行了关联产生一条SQL。
绑定表SQL
那如果不互为绑定表又会是什么情况呢?去掉 spring.shardingsphere.sharding.binding-tables
试一下。
发现控制台解析出了 3条真实表SQL,而去掉 order_id
作为查询条件再次执行后,结果解析出了 9条SQL,进行了笛卡尔积查询。所以相比之下绑定表的优点就不言而喻了。
笛卡尔积查询
如果我一部分表做了分库分表,另一部分未做分库分表的表怎么处理?怎么才能正常访问?
这是一个比较典型的问题,我们知道分库分表是针对某些数据量持续大幅增长的表,比如用户表、订单表等,而不是一刀切将全部表都做分片。那么不分片的表和分片的表如何划分,一般有两种解决方案。
Sharding-JDBC
为例,不给未分片表设置分片规则,它们就不会执行,因为找不到路由规则,这时我们设置一个默认数据源,在找不到规则时一律访问默认库。# 配置数据源 ds-0
spring.shardingsphere.datasource.ds-0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds-0.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds-0.url=jdbc:mysql://47.94.6.5:3306/ds-0?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
spring.shardingsphere.datasource.ds-0.username=root
spring.shardingsphere.datasource.ds-0.password=root
# 默认数据源,未分片的表默认执行库
spring.shardingsphere.sharding.default-data-source-name=ds-0
复制代码
这篇我们针对具体的SQL使用场景,实践一下4种分片策略的用法,开始前先做点准备工作。
标准分片策略
复合分片策略
行表达式分片策略
Hint分片策略
先创建两个数据库 ds-0
、ds-1
,两个库中分别建表 t_order_0
、t_order_1
、t_order_2
、t_order_item_0
、t_order_item_1
、t_order_item_2
6张表,下边实操看看如何在不同场景下应用 sharding-jdbc
的 4种分片策略。
t_order_n
表结构如下:
CREATE TABLE `t_order_0` (
`order_id` bigint(200) NOT NULL,
`order_no` varchar(100) DEFAULT NULL,
`user_id` bigint(200) NOT NULL,
`create_name` varchar(50) DEFAULT NULL,
`price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
复制代码
t_order_item_n
表结构如下:
CREATE TABLE `t_order_item_0` (
`item_id` bigint(100) NOT NULL,
`order_id` bigint(200) NOT NULL,
`order_no` varchar(200) NOT NULL,
`item_name` varchar(50) DEFAULT NULL,
`price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
复制代码
分片策略分为分表策略
和分库策略
,它们实现分片算法的方式基本相同,不同是一个对库ds-0
、ds-1
,一个对表 t_order_0
··· t_order_n
等做处理。
使用场景:SQL 语句中有>
,>=
, <=
,<
,=
,IN
和 BETWEEN AND
操作符,都可以应用此分片策略。
标准分片策略(StandardShardingStrategy
),它只支持对单个分片健(字段)为依据的分库分表,并提供了两种分片算法 PreciseShardingAlgorithm
(精准分片)和 RangeShardingAlgorithm
(范围分片)。
在使用标准分片策略时,精准分片算法是必须实现的算法,用于 SQL 含有 =
和 IN
的分片处理;范围分片算法是非必选的,用于处理含有 BETWEEN AND
的分片处理。
一旦我们没配置范围分片算法,而 SQL 中又用到
BETWEEN AND
或者like
等,那么 SQL 将按全库、表路由的方式逐一执行,查询性能会很差需要特别注意。
接下来自定义实现 精准分片算法
和 范围分片算法
。
1.1 精准分库算法
实现自定义精准分库、分表算法的方式大致相同,都要实现 PreciseShardingAlgorithm
接口,并重写 doSharding()
方法,只是配置稍有不同,而且它只是个空方法,得我们自行处理分库、分表逻辑。其他分片策略亦如此。
SELECT * FROM t_order where order_id = 1 or order_id in (1,2,3);
复制代码
下边我们实现精准分库策略,通过对分片健 order_id
取模的方式(怎么实现看自己喜欢)计算出 SQL 该路由到哪个库,计算出的分片库信息会存放在分片上下文中,方便后续分表中使用。
/**
* @author xiaofu 公众号【程序员内点事】
* @description 自定义标准分库策略
* @date 2020/10/30 13:48
*/
public class MyDBPreciseShardingAlgorithm implements PreciseShardingAlgorithm {
@Override
public String doSharding(Collection databaseNames, PreciseShardingValue shardingValue) {
/**
* databaseNames 所有分片库的集合
* shardingValue 为分片属性,其中 logicTableName 为逻辑表,columnName 分片健(字段),value 为从 SQL 中解析出的分片健的值
*/
for (String databaseName : databaseNames) {
String value = shardingValue.getValue() % databaseNames.size() + "";
if (databaseName.endsWith(value)) {
return databaseName;
}
}
throw new IllegalArgumentException();
}
}
复制代码
其中 Collection
参数在几种分片策略中使用一致,在分库时值为所有分片库的集合 databaseNames
,分表时为对应分片库中所有分片表的集合 tablesNames
;PreciseShardingValue
为分片属性,其中 logicTableName
为逻辑表,columnName
分片健(字段),value
为从 SQL 中解析出的分片健的值。
而 application.properties
配置文件中只需修改分库策略名 database-strategy
为标准模式 standard
,分片算法 standard.precise-algorithm-class-name
为自定义的精准分库算法类路径。
### 分库策略
# 分库分片健
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.sharding-column=order_id
# 分库分片算法
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.precise-algorithm-class-name=com.xiaofu.sharding.algorithm.dbAlgorithm.MyDBPreciseShardingAlgorithm
复制代码
1.2 精准分表算法
精准分表算法同样实现 PreciseShardingAlgorithm
接口,并重写 doSharding()
方法。
/**
* @author xiaofu 公众号【程序员内点事】
* @description 自定义标准分表策略
* @date 2020/10/30 13:48
*/
public class MyTablePreciseShardingAlgorithm implements PreciseShardingAlgorithm {
@Override
public String doSharding(Collection tableNames, PreciseShardingValue shardingValue) {
/**
* tableNames 对应分片库中所有分片表的集合
* shardingValue 为分片属性,其中 logicTableName 为逻辑表,columnName 分片健(字段),value 为从 SQL 中解析出的分片健的值
*/
for (String tableName : tableNames) {
/**
* 取模算法,分片健 % 表数量
*/
String value = shardingValue.getValue() % tableNames.size() + "";
if (tableName.endsWith(value)) {
return tableName;
}
}
throw new IllegalArgumentException();
}
}
复制代码
分表时 Collection
参数为上边计算出的分片库,对应的所有分片表的集合 tablesNames
;PreciseShardingValue
为分片属性,其中 logicTableName
为逻辑表,columnName
分片健(字段),value
为从 SQL 中解析出的分片健的值。
application.properties
配置文件也只需修改分表策略名 database-strategy
为标准模式 standard
,分片算法 standard.precise-algorithm-class-name
为自定义的精准分表算法类路径。
# 分表策略
# 分表分片健
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.sharding-column=order_id
# 分表算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name=com.xiaofu.sharding.algorithm.tableAlgorithm.MyTablePreciseShardingAlgorithm
复制代码
看到这不难发现,自定义分库和分表算法的实现基本是一样的,所以后边我们只演示分库即可
使用场景:当我们 SQL中的分片健字段用到 BETWEEN AND
操作符会使用到此算法,会根据 SQL中给出的分片健值范围值处理分库、分表逻辑。
SELECT * FROM t_order where order_id BETWEEN 1 AND 100;
复制代码
自定义范围分片算法需实现 RangeShardingAlgorithm
接口,重写 doSharding()
方法,下边我通过遍历分片健值区间,计算每一个分库、分表逻辑。
/**
* @author xinzhifu
* @description 范围分库算法
* @date 2020/11/2 12:06
*/
public class MyDBRangeShardingAlgorithm implements RangeShardingAlgorithm {
@Override
public Collection doSharding(Collection databaseNames, RangeShardingValue rangeShardingValue) {
Set result = new LinkedHashSet<>();
// between and 的起始值
int lower = rangeShardingValue.getValueRange().lowerEndpoint();
int upper = rangeShardingValue.getValueRange().upperEndpoint();
// 循环范围计算分库逻辑
for (int i = lower; i <= upper; i++) {
for (String databaseName : databaseNames) {
if (databaseName.endsWith(i % databaseNames.size() + "")) {
result.add(databaseName);
}
}
}
return result;
}
}
复制代码
和上边的一样 Collection
在分库、分表时分别代表分片库名和表名集合,RangeShardingValue
这里取值方式稍有不同, lowerEndpoint
表示起始值, upperEndpoint
表示截止值。
在配置上由于范围分片算法和精准分片算法,同在标准分片策略下使用,所以只需添加上 range-algorithm-class-name
自定义范围分片算法类路径即可。
# 精准分片算法
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.precise-algorithm-class-name=com.xiaofu.sharding.algorithm.dbAlgorithm.MyDBPreciseShardingAlgorithm
# 范围分片算法
spring.shardingsphere.sharding.tables.t_order.database-strategy.standard.range-algorithm-class-name=com.xiaofu.sharding.algorithm.dbAlgorithm.MyDBRangeShardingAlgorithm
复制代码
使用场景:SQL 语句中有>
,>=
, <=
,<
,=
,IN
和 BETWEEN AND
等操作符,不同的是复合分片策略支持对多个分片健操作。
下面我们实现同时以 order_id
、user_id
两个字段作为分片健,自定义复合分片策略。
SELECT * FROM t_order where user_id =0 and order_id = 1;
复制代码
我们先修改一下原配置,complex.sharding-column
切换成 complex.sharding-columns
复数,分片健上再加一个 user_id
,分片策略名变更为 complex
,complex.algorithm-class-name
替换成我们自定义的复合分片算法。
### 分库策略
# order_id,user_id 同时作为分库分片健
spring.shardingsphere.sharding.tables.t_order.database-strategy.complex.sharding-column=order_id,user_id
# 复合分片算法
spring.shardingsphere.sharding.tables.t_order.database-strategy.complex.algorithm-class-name=com.xiaofu.sharding.algorithm.dbAlgorithm.MyDBComplexKeysShardingAlgorithm
复制代码
自定义复合分片策略要实现 ComplexKeysShardingAlgorithm
接口,重新 doSharding()
方法。
/**
* @author xiaofu 公众号【程序员内点事】
* @description 自定义复合分库策略
* @date 2020/10/30 13:48
*/
public class MyDBComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm {
@Override
public Collection doSharding(Collection databaseNames, ComplexKeysShardingValue complexKeysShardingValue) {
// 得到每个分片健对应的值
Collection orderIdValues = this.getShardingValue(complexKeysShardingValue, "order_id");
Collection userIdValues = this.getShardingValue(complexKeysShardingValue, "user_id");
List shardingSuffix = new ArrayList<>();
// 对两个分片健同时取模的方式分库
for (Integer userId : userIdValues) {
for (Integer orderId : orderIdValues) {
String suffix = userId % 2 + "_" + orderId % 2;
for (String databaseName : databaseNames) {
if (databaseName.endsWith(suffix)) {
shardingSuffix.add(databaseName);
}
}
}
}
return shardingSuffix;
}
private Collection getShardingValue(ComplexKeysShardingValue shardingValues, final String key) {
Collection valueSet = new ArrayList<>();
Map> columnNameAndShardingValuesMap = shardingValues.getColumnNameAndShardingValuesMap();
if (columnNameAndShardingValuesMap.containsKey(key)) {
valueSet.addAll(columnNameAndShardingValuesMap.get(key));
}
return valueSet;
}
}
复制代码
Collection
用法还是老样子,由于支持多分片健 ComplexKeysShardingValue
分片属性内用一个分片健为 key
,分片健值为 value
的 map
来存储分片键属性。
行表达式分片策略(InlineShardingStrategy
),在配置中使用 Groovy
表达式,提供对 SQL语句中的 =
和 IN
的分片操作支持,它只支持单分片健。
行表达式分片策略适用于做简单的分片算法,无需自定义分片算法,省去了繁琐的代码开发,是几种分片策略中最为简单的。
它的配置相当简洁,这种分片策略利用inline.algorithm-expression
书写表达式。
比如:ds-$->{order_id % 2}
表示对 order_id
做取模计算,$
是个通配符用来承接取模结果,最终计算出分库ds-0
··· ds-n
,整体来说比较简单。
# 行表达式分片键
sharding.jdbc.config.sharding.tables.t_order.database-strategy.inline.sharding-column=order_id
# 表达式算法
sharding.jdbc.config.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds-$->{order_id % 2}
复制代码
Hint分片策略(HintShardingStrategy
)相比于上面几种分片策略稍有不同,这种分片策略无需配置分片健,分片健值也不再从 SQL中解析,而是由外部指定分片信息,让 SQL在指定的分库、分表中执行。ShardingSphere
通过 Hint
API实现指定操作,实际上就是把分片规则tablerule
、databaserule
由集中配置变成了个性化配置。
举个例子,如果我们希望订单表t_order
用 user_id
做分片健进行分库分表,但是 t_order
表中却没有 user_id
这个字段,这时可以通过 Hint API 在外部手动指定分片健或分片库。
下边我们这边给一条无分片条件的SQL,看如何指定分片健让它路由到指定库表。
SELECT * FROM t_order;
复制代码
使用 Hint分片策略同样需要自定义,实现 HintShardingAlgorithm
接口并重写 doSharding()
方法。
/**
* @author xinzhifu
* @description hit分表算法
* @date 2020/11/2 12:06
*/
public class MyTableHintShardingAlgorithm implements HintShardingAlgorithm {
@Override
public Collection doSharding(Collection tableNames, HintShardingValue hintShardingValue) {
Collection result = new ArrayList<>();
for (String tableName : tableNames) {
for (String shardingValue : hintShardingValue.getValues()) {
if (tableName.endsWith(String.valueOf(Long.valueOf(shardingValue) % tableNames.size()))) {
result.add(tableName);
}
}
}
return result;
}
}
复制代码
自定义完算法只实现了一部分,还需要在调用 SQL 前通过 HintManager
指定分库、分表信息。由于每次添加的规则都放在 ThreadLocal
内,所以要先执行 clear()
清除掉上一次的规则,否则会报错;addDatabaseShardingValue
设置分库分片健键值,addTableShardingValue
设置分表分片健键值。setMasterRouteOnly
读写分离强制读主库,避免造成主从复制导致的延迟。
// 清除掉上一次的规则,否则会报错
HintManager.clear();
// HintManager API 工具类实例
HintManager hintManager = HintManager.getInstance();
// 直接指定对应具体的数据库
hintManager.addDatabaseShardingValue("ds",0);
// 设置表的分片健
hintManager.addTableShardingValue("t_order" , 0);
hintManager.addTableShardingValue("t_order" , 1);
hintManager.addTableShardingValue("t_order" , 2);
// 在读写分离数据库中,Hint 可以强制读主库
hintManager.setMasterRouteOnly();
复制代码
debug 调试看到,我们对 t_order
表设置分表分片健键值,可以在自定义的算法 HintShardingValue
参数中成功拿到。
properties
文件中配置无需再指定分片健,只需自定义的 Hint分片算法类路径即可。
# Hint分片算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.hint.algorithm-class-name=com.xiaofu.sharding.algorithm.tableAlgorithm.MyTableHintShardingAlgorithm
复制代码