ShardingSphere-JDBC使用前后差异

问题

单表数据量很大时,查询的速度会逐渐变慢,且索引已不能很好的优化查询速度,此时可以使用分库分表的方式降低查询压力。现有以下三种情景:

  • 新开发一个使用分库分表的项目
  • 已有一个单库的项目,现需要使用ShardingSphere-JDBC进行分库分表
  • 已有一个使用ShardingSphere-JDBC进行分库分表的项目,现需要将其改回单库使用

根据以上三种情景,可以得出一些问题:

  1. 如何进行分库分表?
  2. 将一个单库项目进行分库分表,需要哪些变动?
  3. 将一个分库分表的项目改为单库运行,需要哪些变动?
  4. 有没有一种方法,可以不变动代码,仅变动配置和依赖等,使项目既可以分库分表情况下运行,又可以单库情况下运行?

分析及方案

ShardingSphere-JDBC是一个几乎只需要引入配置和依赖就可以进行分库分表的工具,问题1只需要参考文档进行开发即可,不在讨论重点之列。

从实践中得知,正常使用单库情况下,SQL几乎是不做限制的,而分库分表情况下,对于SQL的限制更为严苛,这是分库分表的特性及工具本身不够完善带来的结果。

对于问题2、3,我们需要了解一个分库分表的项目与一个单库单表的项目有何差异,需要对然后进行变动。

对于问题4,目前提出的一个方案是,对比两种情况的差异,取两者都可用的交集进行开发,这样在实际使用中,可以不改变代码和SQL,仅修改配置和依赖就可以适用两种情况。这一方案显然会增加开发成本,在实际开发过程中,需要开发者自行评估是否采用。

显然要解决上面的问题,必须了解两种情况下存在的差异,现有的使用差异点总结如下:

  1. 依赖及配置
  2. 主键使用
  3. 函数使用
  4. 分页
  5. 部分SQL不支持的数据类型
  6. 明确不支持的SQL

下文中,将假设已有一个开发好的项目,说明在使用ShardingSphere-JDBC对其进行分库分表过程中,项目需要进行哪些可能的变动及需注意哪些事项。

依赖及配置

首先引入依赖,并进行数据源配置、算法配置、分片策略配置等,此处可参考ShardingSphere-JDBC使用文档。

需注意,配置了ShardingSphere-JDBC的数据源后,无需再配置其他数据源。

主键

常规的主键使用方式

分库分表后,需要改变主键的生成方式。单库模式下比较常用的主键获取方法是函数和序列,例如:

#oracle
insert into t_order (order_id , order_type, order_price, order_time, oper) 
values (order_id_seq.nextval,?,?,?,?)
#mysql
insert into t_order (order_id , order_type, order_price, order_time, oper) 
values (nextval("order_id_seq"),?,?,?,?)

ShardingSphere-JDBC的分布式主键接口

上面两种方式在单库分多个表的情况下可行,但在多库的情况下,不同库中各自维护了递增的序列,将会导致主键的冲突。这样的主键冲突是逻辑上的冲突,虽然不会直接抛出主键冲突的异常,但将多个分表视作一个整表时,会发现表中的主键出现重复。

多库情况下保证主键不重复是第一个需要解决的问题,ShardingSphere-JDBC提供了分布式主键生成算法的接口,可以通过实现该接口并进行配置(此处详细参考“ShardingSphere-JDBC使用文档”),在insert时生成主键值。ShardingSphere-JDBC提供了UUID和snowflake算法的实现,可以直接配置使用。如果要自行实现主键生成算法,需注意多库情况下的主键一致。

如果配置了ShardingSphere-JDBC的分布式主键算法实现,需要将SQL进行改写,将主键列删除,例如:

insert into t_order (order_type, order_price, order_time, oper) 
values (?,?,?,?)

在代码中设置主键

使用ShardingSphere-JDBC的分布式主键有一个问题,一旦使用,将分库分表切回单库情况时,SQL显然是不适用的,如果要保证两种情况主键都适用,一个比较有效的方案是,在代码中就设置好主键。现在假设有一个主键生成接口,该接口有多个实现,可以通过配置修改获取主键的方法,例如:

//假设getKey是自定义的主键获取接口,根据配置注入实现类对象。
order.setOrderId(getKeyInterface.getKey(order));
...

此时SQL如下:

insert into t_order (order_id , order_type, order_price, order_time, oper) 
values (?,?,?,?,?)

这一方法可以保证两种情况下代码和SQL都不需要改动,只需要修改配置改变获取主键的实现类即可。

函数

本小节针对自定义函数调用的情况进行说明。

单库情况下,函数都在一个库中,可正常调用函数。

ShardingSphere-JDBC 分库分表时,如果有多个库,此时区分为以下两种情况:

  1. 在SQL中仅调用函数,此时调用函数时会随机选取其中一个库调用一次,此时如果使用函数对库中的数据进行修改或查询类的操作,可能导致操作的结果错误。SQL示例如下:

    #假设sum_id是统计某几个表数据量的函数,此时仅会随机统计一个库的数据返回
    select count_id()
    
  2. 在增删查改中调用函数,此时调用表所在库的该函数,调用正常。SQL示例如下:

    select ...,count_id() from t_order,t_user where ...
    

分页

如果在项目中使用了 KOCA 提供的分页组件,此项可略过。如果自行实现了分页,建议参考 KOCA 的分页执行SQL进行适当变动。原因是在ShardingSphere-JDBC执行分页SQL过程中,如果SQL的格式解析异常,可能导致分页执行失败,或返回的结果集合并出错。

另外需要注意,在ShardingSphere-JDBC 5.0.0-RC1版本前(包括4.0版本及5.0.0-alpha、bate版本),oracle分页结果集合并均有异常,建议使用5.0.0-RC1版本。

部分SQL不支持的数据类型

单库执行查询SQL时,任意SQL的任意类型都支持,但在ShardingSphere-JDBC执行某些SQL时不支持部分数据类型,为了将这些类型的数据正常查询出来,需要调整SQL。

ShardingSphere-JDBC 在执行SQL的过程中,存在一些解析和执行的分支,解析执行某些类型的SQL使用的三方解析工具calcite,对时间类型和大文本类型支持不是很好,此时需要调整SQL,使执行时该SQL不使用calcite进行解析。

如果一个已开发好的项目使用 ShardingSphere-JDBC 分库分表,这一步骤可能需要对SQL进行许多变动。

建议参考“ShardingSphere-JDBC使用”的第六节第四小节,调整会返回时间类型的SQL为不会被calcite解析的类型。

不支持的SQL

此处可参考“ShardingSphere-JDBC使用”的第六节第五小节,对不支持的SQL进行调整。

查询效率

查询时的笛卡尔积关联

分库分表本身用于单表数据量过大,查询效率过慢的问题。

ShardingSphere-JDBC的分库分表在执行查询时,如果关联查询的几个表不是绑定表(绑定表定义见“ShardingSphere-JDBC使用文档”名词解释),或查询不携带分片键作为条件,会导致全路由,即数据库中逻辑表的实际表节点进行笛卡尔积关联。例如:

select ... from t_order,t_user,t_detail where ...

假定这一SQL中,三个表之间没有绑定关系,且它们同时进行了分库和分表,在db_0、db_1两个库中都各自拥有XX_0、XX_1两个表,那么在查询时,实际执行的语句为:

select ... from db0.t_order_1,db0.t_user_0,db0.t_detail_0 where ...
select ... from db0.t_order_1,db0.t_user_0,db0.t_detail_1 where ...
select ... from db0.t_order_1,db0.t_user_1,db0.t_detail_0 where ...
select ... from db0.t_order_1,db0.t_user_1,db0.t_detail_1 where ...
select ... from db0.t_order_0,db0.t_user_0,db0.t_detail_0 where ...
select ... from db0.t_order_0,db0.t_user_0,db0.t_detail_1 where ...
select ... from db0.t_order_0,db0.t_user_1,db0.t_detail_0 where ...
select ... from db0.t_order_0,db0.t_user_1,db0.t_detail_1 where ...
select ... from db1.t_order_1,db1.t_user_0,db1.t_detail_0 where ...
select ... from db1.t_order_1,db1.t_user_0,db1.t_detail_1 where ...
select ... from db1.t_order_1,db1.t_user_1,db1.t_detail_0 where ...
select ... from db1.t_order_1,db1.t_user_1,db1.t_detail_1 where ...
select ... from db1.t_order_0,db1.t_user_0,db1.t_detail_0 where ...
select ... from db1.t_order_0,db1.t_user_0,db1.t_detail_1 where ...
select ... from db1.t_order_0,db1.t_user_1,db1.t_detail_0 where ...
select ... from db1.t_order_0,db1.t_user_1,db1.t_detail_1 where ...

以上执行了共16个SQL。此时可能出现的问题是,如果原单表数据量没那么大,而随意进行了分库分表,可能导致分库分表后实际执行效率低于从前。

绑定表优化查询

执行时为了保证数据正确,对不同表进行笛卡尔关联是必要的,ShardingSphere-JDBC对这样的情景,提出了绑定表配置进行优化。如果上述例子中,三个表是绑定表(绑定表的相关配置见“ShardingSphere-JDBC使用”配置相关内容),它们的分片规则一致,则此时实际执行的语句为:

select ... from db0.t_order_1,db0.t_user_1,db0.t_detail_1 where ...
select ... from db0.t_order_0,db0.t_user_0,db0.t_detail_0 where ...
select ... from db1.t_order_1,db1.t_user_1,db1.t_detail_1 where ...
select ... from db1.t_order_0,db1.t_user_0,db1.t_detail_0 where ...

共四个SQL,执行效率大大提升。

仅分库或分表时的查询

现在继续假设三个表是非绑定表,调整分库分表的数量以减少实际执行的查询数。

假设三个表只做了单库分表,在db_0中各自拥有XX_0、XX_1两个表,则实际执行的SQL为:

select ... from db0.t_order_1,db0.t_user_0,db0.t_detail_0 where ...
select ... from db0.t_order_1,db0.t_user_0,db0.t_detail_1 where ...
select ... from db0.t_order_1,db0.t_user_1,db0.t_detail_0 where ...
select ... from db0.t_order_1,db0.t_user_1,db0.t_detail_1 where ...
select ... from db0.t_order_0,db0.t_user_0,db0.t_detail_0 where ...
select ... from db0.t_order_0,db0.t_user_0,db0.t_detail_1 where ...
select ... from db0.t_order_0,db0.t_user_1,db0.t_detail_0 where ...
select ... from db0.t_order_0,db0.t_user_1,db0.t_detail_1 where ...

实际共执行了八个SQL。

再假设三个表只执行了分库,而没有分表,在db_0、db_1两个库中都各自拥有一个单表,此时执行的SQL为:

select ... from db0.t_order,db0.t_user,db0.t_detail where ...
select ... from db1.t_order,db1.t_user,db1.t_detail where ...

实际执行的数量更少。

查询效率总结

上述的例子说明,选择合适的分片规则和写查询SQL需要一定的考量,随意的分表、不当SQL和配置可能导致查询效率下降。生产过程中,开发者应该根据实际状况分库分表并编写SQL,并适当设置绑定表提高查询效率。