问题
单表数据量很大时,查询的速度会逐渐变慢,且索引已不能很好的优化查询速度,此时可以使用分库分表的方式降低查询压力。现有以下三种情景:
- 新开发一个使用分库分表的项目
- 已有一个单库的项目,现需要使用ShardingSphere-JDBC进行分库分表
- 已有一个使用ShardingSphere-JDBC进行分库分表的项目,现需要将其改回单库使用
根据以上三种情景,可以得出一些问题:
- 如何进行分库分表?
- 将一个单库项目进行分库分表,需要哪些变动?
- 将一个分库分表的项目改为单库运行,需要哪些变动?
- 有没有一种方法,可以不变动代码,仅变动配置和依赖等,使项目既可以分库分表情况下运行,又可以单库情况下运行?
分析及方案
ShardingSphere-JDBC是一个几乎只需要引入配置和依赖就可以进行分库分表的工具,问题1只需要参考文档进行开发即可,不在讨论重点之列。
从实践中得知,正常使用单库情况下,SQL几乎是不做限制的,而分库分表情况下,对于SQL的限制更为严苛,这是分库分表的特性及工具本身不够完善带来的结果。
对于问题2、3,我们需要了解一个分库分表的项目与一个单库单表的项目有何差异,需要对然后进行变动。
对于问题4,目前提出的一个方案是,对比两种情况的差异,取两者都可用的交集进行开发,这样在实际使用中,可以不改变代码和SQL,仅修改配置和依赖就可以适用两种情况。这一方案显然会增加开发成本,在实际开发过程中,需要开发者自行评估是否采用。
显然要解决上面的问题,必须了解两种情况下存在的差异,现有的使用差异点总结如下:
- 依赖及配置
- 主键使用
- 函数使用
- 分页
- 部分SQL不支持的数据类型
- 明确不支持的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 分库分表时,如果有多个库,此时区分为以下两种情况:
-
在SQL中仅调用函数,此时调用函数时会随机选取其中一个库调用一次,此时如果使用函数对库中的数据进行修改或查询类的操作,可能导致操作的结果错误。SQL示例如下:
#假设sum_id是统计某几个表数据量的函数,此时仅会随机统计一个库的数据返回 select count_id()
-
在增删查改中调用函数,此时调用表所在库的该函数,调用正常。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,并适当设置绑定表提高查询效率。