本文档用于介绍ShardingSphere-JDBC分库分表在springboot上的使用及使用时注意事项,前面的小节提供快速入门说明,最后一小节提供了一些使用说明及建议。此外更多内容可参考ShardingSphere 5.0 版本的官方文档:https://shardingsphere.apache.org/document/current/cn/overview/
概述
ShardingSphere-JDBC 是一款开源的分库分表工具,当单个表数据量增大,读写需要的时间也越多,此时可以分库分表来分担单表时数据库的压力。
ShardingSphere-JDBC 的设计理念之一,是让使用者在使用过程中,仍可以将几个数据源视为一个整的数据源,即开发者在开发过程中,无需更改业务代码,编写的SQL中不体现出使用了多个数据库,只需要配置分库分表的规则,即可达成目的。
ShardingSphere-JDBC 可基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等,且适用于任何基于Java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
名词解释
分片键:指分库分表时用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例如根据订单类型(order_type)进行分库分表,此时的分片键就是order_type。
逻辑表和真实表:逻辑表是水平拆分的数据库(表)的相同逻辑和数据结构表的总称,真实表是数据库中实际包含的表。例:订单数据根据主键取余拆分为 2 张表,分别是 t_order_0 和 t_order_1,他们的逻辑表名为 t_order,真实表即为t_order_0和t_order_1。
绑定表:指分片规则一致的主表和子表。例如:有t_order 表和 t_order_detail 表,均按照 order_id 分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
单表:指所有的分片数据源中只存在唯一一张的表。适用于数据量不大且不需要做任何分片操作的场景。
广播表:指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
数据节点:数据分片的最小单元。由数据源名称和数据表组成,如mydatabase1.t_order_0。
依赖导入
需要注意,从sharding-jdbc 4.0 升级到 ShardingSphere-JDBC 5.0 后,不止版本号发生了变化,maven坐标也发生了变化,springboot上使用ShardingSphere-JDBC 时使用依赖的坐标:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-spring-namespace-infra</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
版本说明:ShardingSphere-JDBC 的最新发布版本是5.0.0-bate,但更建议使用正在进行的版本5.0.0-RC1,该版本相较5.0.0-bate修复了大量bug。
使用说明
使用ShardingSphere-JDBC时 ,只需要进行相关配置即可直接运行,不需要修改业务代码。
由于目前ShardingSphere-JDBC对SQL并非全部支持,可能需要对SQL进行相应的检查和变动。需要注意的内容见章节“使用事项说明及建议”。
配置说明
一、数据源配置
spring:
shardingsphere:
# 数据源配置
datasource:
names: mydatabase0,mydatabase1
mydatabase0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db0?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&allowMultiQueries=true
username: root
password: 123456
mydatabase1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&allowMultiQueries=true
username: root
password: 123456
二、绑定表及广播表配置
需注意,广播表如不正确配置,可能直接导致项目无法启动成功
spring:
shardingsphere:
rules:
sharding:
#绑定表配置,配置一组绑定表,此处配置逻辑表名,对应下面tables下配置的表名
binding-tables:
- T_ORDER,T_ORDER_DETAIL_
#广播表配置,多个数据库中相同的表配置为广播表,如字典表
broadcast-tables: demo_sequence,t_typedemo_1,t_typedemo_0
三、分库分表规则配置
在配置过程中,如果使用的是oracle数据库,建议将逻辑表名写为大写,实际数据节点按实际表名的大小写配置,这一建议的原因见“使用事项说明及建议”章节的“Apache Calcite 解析SQL”小节。
一个完整的分库分表配置示例如下:
spring:
shardingsphere:
rules:
sharding:
#分表策略配置
tables:
T_ORDER:
logic-table: T_ORDER
#实际数据节点,此处对应mydatabase1.t_order_0、mydatabase1.t_order_1、mydatabase0.t_order_0、mydatabase0.t_order_1
actual-data-nodes: mydatabase$->{0..1}.t_order_$->{0..1}
#分表策略
table-strategy:
standard:
#分片键
sharding-column: order_id
#分表算法
sharding-algorithm-name: t_order_sharding
#分库策略
database-strategy:
standard:
sharding-column: order_id
#分库算法
sharding-algorithm-name: db_sharding
#分布式主键策略配置
key-generate-strategy:
#主键
column: order_id
#分布式主键生成算法
key-generator-name: demokey
上面的配置中,分表算法、分库算法、分布式主键算法均是自定义的算法,下面将展示一个分表算法接口拓展的实践。
四、自定义算法
要实现 ShardingSphere-JDBC 的自定义算法,首先需要了解SPI机制,SPI的全名为:Service Provider Interface,是一种服务发现机制。简单来说是在包内定义接口,在使用中提供具体实现类,并在META-INF\services文件夹中,添加一个文件,文件名为接口全类名,内容为实现类全类名。运行时,会根据该文件的配置,找到该接口的实现类。
ShardingSphere-JDBC 中所有可拓展的接口均使用该机制进行拓展,接下来使用StandardShardingAlgorithm
接口的拓展做为示例:
(一)实现 StandardShardingAlgorithm
接口
public class DemoPreciseShardingAlgorithm implements StandardShardingAlgorithm {
@Override
public Collection<String> doSharding(Collection collection, RangeShardingValue rangeShardingValue) {
return collection;
}
@Override
public String doSharding(Collection collection, PreciseShardingValue preciseShardingValue) {
//collection包含:配置的分片的节点名称,preciseShardingValue包含:逻辑表名、分片字段、字段值
Object o = collection.toArray()[(int) (Long.parseLong(String.valueOf(preciseShardingValue.getValue())) % collection.size())];
return String.valueOf(o);
}
@Override
public void init() {
}
@Override
public String getType() {
//此方法返回的字符串将被用于后续算法配置中
return "DEMO_SHARDING-T";
}
}
(二)配置services
在META-INF\services新建名为 org.apache.shardingsphere.sharding.spi.ShardingAlgorithm
的文件,内容为实现类的全类名:
com.szkingdom.sample.shardingsphere.strategy.DemoPreciseShardingAlgorithm
(三)在配置文件中配置该算法
spring:
shardingsphere:
rules:
sharding:
#自定义算法配置
sharding-algorithms:
#t_order_sharding作为该分表算法的名称,用于后续配置
t_order_sharding:
#type对应实现接口时getType返回的字符串
type: DEMO_SHARDING-T
(四)分表策略使用该算法
spring:
shardingsphere:
rules:
sharding:
tables:
T_ORDER:
actual-data-nodes: mydatabase$->{0..1}.t_order_$->{0..1}
#分表策略
table-strategy:
standard:
sharding-column: order_id
#此处配置分表算法名称
sharding-algorithm-name: t_order_sharding
根据以上步骤,即可自定义分库分表及分布式主键算法。自定义分布式主键时,第3步需要改用以下方式配置该算法:
spring:
shardingsphere:
rules:
sharding:
#分布式主键配置
key-generators:
snowflake:
type: SNOWFLAKE
demokey:
type: DEMO-KEY
上述配置中,配置了两个分布式主键算法,一个是ShardingSphere已经实现的雪花算法,另一个是我们自定义的算法demokey,该算法用于前文的分库分表配置中。
使用事项说明及建议
一、koca-jdbc与ShardingSphere-JDBC一起使用
同时使用koca-jdbc和ShardingSphere-JDBC时,只需要将默认数据源设置为ShardingSphere-JDBC的数据源,配置参考如下:
jdbc: #使用koca-jdbc时,将默认数据源配置为shardingSphere-JDBC的数据源
defaultDataSourceId: shardingSphereDataSource
dataSources:
二、主键使用建议
使用ShardingSphere-JDBC分库分表时,如果主键是分片键,可能面临一个问题:如果在insert语句中使用序列或函数获取主键,或将主键列设置为递增列,insert语句中没有主键列,分库分表时将会因为获取不到分片键的值而分片失败,因为此时SQL还未真正在数据库执行,主键列值为空。同时,以往的主键生成方式还存在一个问题:主键的序列值只维护在单个的数据库中,可能导致主键的重复。
要解决上述的第一个问题,可以使用ShardingSphere-JDBC的分布式主键功能。在前面的配置说明中可以看到,配置分布式主键时配置了列名和分布式主键策略名称,在实际执行时,insert此表时,会将列名与计算得出的主键值加在原本的SQL中。
因此,如果使用分布式主键,insert的SQL语句不需要写主键列,例如,如果t_order表的主键是order_id,并配置了ShardingSphere-JDBC的分布式主键策略,则SQL应如下所示:
insert into t_order (order_type, order_price, order_time, oper)values ( ? , ? , ? , ? )
除上述方法外,可以在代码中设置主键值,以保证SQL执行前已经明确主键值。但不管使用哪一种,如何保证设置的主键唯一都是开发者需要考虑的事。针对主键唯一的问题,ShardingSphere-JDBC提供了UUID和SNOWFLAKE两种主键生成算法,两种方式均可保证生成的主键不重复,按照前面小节的配置说明配置即可。
三、函数和序列特性
函数和序列并不存在可配置项中,通过实践得知,在访问自定义函数和序列时,如果存在多库情况,在SQL语句中仅调用函数或序列时,ShardingSphere-JDBC会随机访问其中的一个,建议使用函数和序列时考虑访问的随机性影响。仅调用函数的SQL示例如下:
#假设sum_id是统计某几个表数据量的函数
select count_id()
四、Apache Calcite 解析SQL
(一)类型支持说明
在ShardingSphere-JDBC 5.0版本中,引入了Apache Calcite对部分SQL进行解析执行,可以简单的理解为,在SQL执行时,满足一定条件的SQL由Calcite解析执行,剩下的SQL仍由ShardingSphere-JDBC自行实现的引擎解析执行。
此时会出现一些问题,譬如Calcite本身不支持二进制大文本类型(Blob类型)的返回处理,可以使用Object类型接收,但使用常用的byte[]类型接收会报错。在返回类型处理时,时间类型的处理会发生错误。
同时ShardingSphere-JDBC自行解析执行的SQL,基本没有不支持和报错的类型。
因此我们得到一个结论,某些格式的SQL由Calcite解析执行,此时尽量不要查询大文本类型和时间类型。下面将详述一个SQL如何被判定为由Calcite解析。
(二)由Calcite解析的SQL
-
首先,执行的需要是select语句,select的表包含分片规则中配置的表。
-
判断是否包含Having关键字、或是否包含聚合子查询,任意包含其中一个,则由Calcite解析。SQL示例如下:
//包含having子句
select AVG(order_price) avg_, MAX(order_price) max_
from t_order t, t_order_detail_ dwhere t.order_id = d.order_id
group by order_type having MAX(order_price)>100
//包含聚合子查询
SELECT avg_, max_ FROM(SELECT AVG(order_price) avg_, MAX(order_price) max_
FROM t_order t, t_order_detail_ dWHERE t.order_id = d.order_id
GROUP BY order_type) tempWHERE max_ > 100
- 如果上面情况都不包含,则判断是否不包含连接查询和子查询,如果两种都不包含,则该SQL一定不由Calcite解析。SQL示例如下:
select order_id, order_type, order_price, order_time, oper
from t_order twhere t.order_id < 100 order by t.order_id DESC
-
如果包含了连接查询或子查询,判断是否所有表在同一个数据源中,如果所有表在同一个数据源中,则该SQL一定不由Calcite解析。
-
最后判断是否所有表都是分片表且不存在绑定关系,如果是,则由Calcite解析。SQL示例如下:
select t.order_id ,order_type, order_price, oper, order_detail_1 ,order_detail_2
from t_order t, t_order_detail_ d
where t.order_id = d.order_id and t.order_id < 100 order by t.order_id DESC
(三)Calcite使用Blob时接收实体类
建议用不会由Calcite解析的SQL进行Blob和时间类型的查询,如果一定要使用由Calcite解析的SQL查询Blob类型,需要使用Object类型进行接收,即接收的实体类中,该列对应的成员类型为Object。
该成员将被赋值为一个Blob类型的对象,需要自行在代码中将其转换为 Byte[] 类型。例如,OrderDetail类中,orderDetail1对应的数据库列是Blob类型,在get方法中进行了转换处理,将Blob转换为 Byte[] 返回
public class OrderDetail extends Order{
private Object orderDetail1;
private String orderDetail2;
public Object getOrderDetail1() {
if(this.orderDetail1 instanceof Blob) {
try {
return ((Blob) orderDetail1).getBytes(1, (int) ((Blob) orderDetail1).length());
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return orderDetail1;
}
public void setOrderDetail1(Object orderDetail1) {
this.orderDetail1 = orderDetail1;
}
......
}
(四)oracle逻辑表配置建议大写的原因
在使用Calcite做解析时,如果使用的数据库是oracle,对于未带引号的表和字段,将会将其全部置为大写,此时如果逻辑表配置使用小写,将会抛出找不到逻辑表的异常,因此建议使用oracle时,将逻辑表名配置为大写。
逻辑表名置为大写时,执行的SQL语句中表名无需特意大写与其对应,这是因为ShardingSphere-JDBC本身是忽略大小写的,即使SQL中表名和字段名与配置中不一致也可以正常执行。
五、明确的SQL不支持项
这一小节用于说明不支持的SQL类型及建议:
不支持:
UNION 和 UNION ALL,示例SQL如下:
SELECT * FROM t_order UNION SELECT * FROM t_order_detail_
SELECT * FROM t_order UNION ALL SELECT * FROM t_order_detail_
建议:
1、多表查询时,尽量使用表的别名而非直接使用表名,例如查询时使用聚合函数,如果在聚合函数中使用表名,将导致查询失败,但使用表的别名时可以成功。SQL示例如下:
#会失败
select MAX(t_order.order_price) from t_order, t_order_detail_ where t_order.order_id = t_order_detail_.order_id
#执行成功
select MAX(t.order_price) from t_order t, t_order_detail_ d where t.order_id = d.order_id
2、查询时如果条件不携带分片键,可能导致全路由,即对所有的数据节点进行查询,可能导致大量的资源消耗,开发过程中应注意这一点。
六、版本说明
Sharding-JDBC 4.0.0版本发布最早,对复杂SQL的支持性较弱,分页时数据合并也存在较多问题。在ShardingSphere-JDBC 5.0.0-alpha和beta 版本中,分页数据合并的问题仍然存在,但5.0版本对于复杂SQL的支持明显增强。
随着贡献者与使用者的不断增加,当前正在进行的5.0.0-RC1版本已经解决了此前存在的大量bug,包括分页结果数据合并问题。该版本当前尚未发布,但相较最新发布的5.0.0-beta版本,5.0.0-RC1版本使用体验更佳,因此虽然5.0.0-RC1版本仍在快速的变动实现细节,仍更推荐使用此版本。