ShardingSphere-JDBC使用

本文档用于介绍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

  1. 首先,执行的需要是select语句,select的表包含分片规则中配置的表。

  2. 判断是否包含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
  1. 如果上面情况都不包含,则判断是否不包含连接查询和子查询,如果两种都不包含,则该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
  1. 如果包含了连接查询或子查询,判断是否所有表在同一个数据源中,如果所有表在同一个数据源中,则该SQL一定不由Calcite解析。

  2. 最后判断是否所有表都是分片表且不存在绑定关系,如果是,则由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版本仍在快速的变动实现细节,仍更推荐使用此版本。