分片篇:shardingsphere JDBC 5.X 改写引擎 设计思路

工程师在使用 shardingsphere jdbc 时,都是面向逻辑库逻辑表 编写 SQL 的 ,然而这些 SQL 并不能直接在真实的数据库中执行。

路由引擎返回路由结果后,改写引擎会将逻辑 SQL 改写为在真实数据库中可以正确执行的 SQL 。

1 标识符改写

标识符改写最核心的是:表名称改写。表名称改写是指将找到逻辑表在原始SQL中的位置,并将其改写为真实表。

从一个最简单的例子开始,若逻辑 SQL 为:

ini 复制代码
SELECT order_id FROM t_order WHERE order_id = 1;

假设该 SQL 配置分片键 order_id ,并且 order_id=1 的情况,将路由至分片表 1。

那么改写之后的 SQL 应该为:

ini 复制代码
SELECT order_id FROM t_order_1 WHERE order_id = 1;

需要注意的是:表名称修改并不是简单的字符串查找和替换

比如下面的场景,就无法仅仅通过字符串的查找替换来正确的改写 SQL 了:

ini 复制代码
SELECT order_id FROM t_order WHERE order_id=1 AND remarks=' t_order xxx';

正确改写的 SQL 应该是:

ini 复制代码
SELECT order_id FROM t_order_1 WHERE order_id=1 AND remarks=' t_order xxx';

而非:

ini 复制代码
SELECT order_id FROM t_order_1 WHERE order_id=1 AND remarks=' t_order_1 xxx';

我们再来看一个更加复杂的例子,

ini 复制代码
SELECT t_order.order_id FROM t_order WHERE t_order.order_id=1 AND remarks=' t_order xxx';

上面的 SQL 将表名作为字段的标识符,因此在 SQL 改写时需要一并修改:

ini 复制代码
SELECT t_order_1.order_id FROM t_order_1 WHERE t_order_1.order_id=1 AND remarks=' t_order xxx';

而如果SQL中定义了表的别名,则无需连同别名一起修改,即使别名与表名相同亦是如此。例如:

vbnet 复制代码
SELECT t_order.order_id FROM t_order AS t_order WHERE t_order.order_id=1 AND remarks=' t_order xxx';

SQL 改写则仅需要改写表名称就可以了:

vbnet 复制代码
SELECT t_order.order_id FROM t_order_1 AS t_order WHERE t_order.order_id=1 AND remarks=' t_order xxx';

2 补列

补列通常用于查询语句中,分两种场景:

1、分组和排序

ShardingSphere 需要在结果归并时获取相应数据,但该数据并未能通过查询的SQL 返回 。

查询 SQL 语句中,若包含分组或者排序 , 当结果归并时,需要根据GROUP BYORDER BY的字段项进行分组和排序,但如果原始 SQL 的选择项中若并未包含分组项或排序项,则需要对原始SQL进行改写。

先看一下原始 SQL 中带有结果归并所需信息的场景:

vbnet 复制代码
SELECT order_id, user_id FROM t_order ORDER BY user_id;

由于使用 user_id 进行排序,在结果归并中需要能够获取到 user_id 的数据,而上面的SQL是能够获取到user_id数据的,因此无需补列。

如果选择项中不包含结果归并时所需的列,则需要进行补列,如以下 SQL :

vbnet 复制代码
SELECT order_id FROM t_order ORDER BY user_id;

由于原始 SQL 中并不包含需要在结果归并中需要获取的user_id,因此需要对 SQL 进行补列改写。补列之后的 SQL 是:

vbnet 复制代码
SELECT order_id, user_id AS ORDER_BY_DERIVED_0 FROM t_order ORDER BY user_id;

值得一提的是,补列只会补充缺失的列,不会全部补充,而且,在SELECT语句中包含*的SQL,也会根据表的元数据信息选择性补列。

下面是一个较为复杂的 SQL 补列场景:

vbnet 复制代码
SELECT o.* FROM t_order o, t_order_item i WHERE o.order_id=i.order_id ORDER BY user_id, order_item_id;

我们假设只有 t_order_item 表中包含 order_item_id 列,那么根据表的元数据信息可知,在结果归并时,排序项中的 user_id 是存在于 t_order 表中的,无需补列。然而 order_item_id 并不在 t_order 中,因此需要补列。

补列之后的SQL是:

vbnet 复制代码
SELECT o.*, order_item_id AS ORDER_BY_DERIVED_0 FROM t_order o, t_order_item i WHERE o.order_id=i.order_id ORDER BY user_id, order_item_id;

2、AVG 聚合函数

分布式的场景中,使用 avg1 + avg2 + avg3 / 3计算平均值并不正确,需要改写为 (sum1 + sum2 + sum3) / (count1 + count2 + count3)。

这就需要将包含 AVG 的SQL 改写为 SUM 和 COUNT ,并在结果归并时重新计算平均值。

例如以下 SQL :

sql 复制代码
SELECT AVG(price) FROM t_order WHERE user_id = 1;

需要改写为如下 SQL ,才能够通过结果归并正确的计算平均值。

sql 复制代码
SELECT COUNT(price) AS AVG_DERIVED_COUNT_0, SUM(price) AS AVG_DERIVED_SUM_0 FROM t_order WHERE user_id=1;

3、自增主键补列

执行 INSERT 的 SQL 语句时,如果使用数据库自增主键,是无需写入主键字段的。

但数据库的自增主键是无法满足分布式场景下的主键唯一的,因此 ShardingSphere 提供了分布式自增主键的生成策略,并且可以通过补列,让使用方无需改动现有代码,即可将分布式自增主键透明的替换数据库现有的自增主键。

sql 复制代码
INSERT INTO t_order (`field1`, `field2`) VALUES (10, 1);

可以看到,上述SQL中并未包含自增主键,是需要数据库自行填充的。ShardingSphere 配置自增主键后,SQL 将改写为:

sql 复制代码
INSERT INTO t_order (`field1`, `field2`, order_id) VALUES (10, 1, xxxxx);

改写后的 SQL 将在 INSERT FIELD 和 INSERT VALUE 的最后部分增加主键列名称以及自动生成的自增主键值。上述 SQL 中的 xxxxx 表示自动生成的自增主键值。

如果 INSERT 的 SQL 中并未包含表的列名称,ShardingSphere 也可以根据判断参数个数以及表元信息中的列数量对比,并自动生成自增主键。

例如,原始的 SQL 为:

sql 复制代码
INSERT INTO t_order VALUES (10, 1);

改写的 SQL 将只在主键所在的列顺序处增加自增主键即可:

sql 复制代码
INSERT INTO t_order VALUES (xxxxx, 10, 1);

3 分页修正

从多个数据库获取分页数据与单数据库的场景是不同的。 假设每10条数据为一页,取第2页数据。在分片环境下获取LIMIT 10, 10,归并之后再根据排序条件取出前 10 条数据是不正确的。

举例说明,若 SQL 为:

sql 复制代码
SELECT score FROM t_score ORDER BY score DESC LIMIT 1, 2;

下图展示了不进行 SQL 的改写的分页执行结果:

通过图中所示,想要取得两个表中共同的按照分数排序的第 2 条和第 3 条数据,应该是 9590

由于执行的 SQL 只能从每个表中获取第 2 条和第 3 条数据,即从 t_score_0 表中获取的是 9080;从 t_score_1 表中获取的是 8575

因此进行结果归并时,只能从获取的 90808575 之中进行归并,那么结果归并无论怎么实现,都不可能获得正确的结果。

正确的做法是将分页条件改写为 LIMIT 0, 3,取出所有前两页数据,再结合排序条件计算出正确的数据。

下图展示了进行 SQL 改写之后的分页执行结果。

越获取偏移量位置靠后数据,使用LIMIT分页方式的效率就越低。 有很多方法可以避免使用LIMIT进行分页。比如构建行记录数量与行偏移量的二级索引,或使用上次分页数据结尾ID作为下次查询条件的分页方式等。

分页信息修正时,如果使用占位符的方式书写 SQL,则只需要改写参数列表即可,无需改写 SQL 本身。

4 批量拆分

在使用批量插入的 SQL 时,如果插入的数据是跨分片的,那么需要对 SQL 进行改写来防止将多余的数据写入到数据库中。插入操作与查询操作的不同之处在于,查询语句中即使用了不存在于当前分片的分片键,也不会对数据产生影响;而插入操作则必须将多余的分片键删除。举例说明,如下 SQL:

sql 复制代码
INSERT INTO t_order (order_id, xxx) VALUES (1, 'xxx'), (2, 'xxx'), (3, 'xxx');

假设数据库仍然是按照 order_id 的奇偶值分为两片的,仅将这条 SQL 中的表名进行修改,然后发送至数据库完成 SQL 的执行,则两个分片都会写入相同的记录。虽然只有符合分片查询条件的数据才能够被查询语句取出,但存在冗余数据的实现方案并不合理。因此需要将 SQL 改写为:

sql 复制代码
INSERT INTO t_order_0 (order_id, xxx) VALUES (2, 'xxx');
INSERT INTO t_order_1 (order_id, xxx) VALUES (1, 'xxx'), (3, 'xxx');

使用 IN 的查询与批量插入的情况相似,不过 IN 操作并不会导致数据查询结果错误。通过对 IN 查询的改写,可以进一步的提升查询性能。如以下 SQL:

sql 复制代码
SELECT * FROM t_order WHERE order_id IN (1, 2, 3);

改写为:

sql 复制代码
SELECT * FROM t_order_0 WHERE order_id IN (2);
SELECT * FROM t_order_1 WHERE order_id IN (1, 3);

可以进一步提升查询性能。ShardingSphere 暂时还未实现此改写策略,目前的改写结果是:

sql 复制代码
SELECT * FROM t_order_0 WHERE order_id IN (1, 2, 3);
SELECT * FROM t_order_1 WHERE order_id IN (1, 2, 3);

虽然 SQL 的执行结果是正确的,但并未达到最优的查询效率。

5 总结

这篇文章,我们简单介绍了重写引擎的设计思路。

1、标识符改写

核心是表名称改写,并不是通过简单的字符串查询和替换来执行的。

2、补列

补列通常用于查询语句中,分三种场景:

  • 分组和排序
  • AVG 聚合函数
  • 自增主键补列

3、分页修正

查询分页语句中,分布式场景下,为了保证结果正确,需要对分页参数进行修改。

4、 批量拆分

用批量插入的 SQL 时,如果插入的数据是跨分片的,那么需要对 SQL 进行改写来防止将多余的数据写入到数据库中。


下篇文章,笔者会通过源码详解重写引擎的原理,敬请期待。

相关推荐
java66666888837 分钟前
深入理解Spring Boot中的配置加载顺序
java·spring boot·后端
春山之外41 分钟前
基于IIS的Windows系统Django项目本地部署
后端·python·django·iis·服务器部署
空青7261 小时前
ChatGPT在Java后端开发中的应用与影响
java·开发语言·人工智能·后端·神经网络·机器学习·chatgpt
冯宝宝^1 小时前
图书管理系统
服务器·数据库·vue.js·spring boot·后端
java6666688882 小时前
深入理解Spring Boot中的容器与依赖注入
java·spring boot·后端
u0104058362 小时前
Spring Boot中的依赖注入和控制反转
java·spring boot·后端
虫小宝2 小时前
解决Spring Boot中的安全漏洞与防护策略
java·spring boot·后端
test6382 小时前
使用ThreadLocal存储用户登录信息
java·后端·面试
续亮~3 小时前
6、Redis系统-数据结构-06-跳表
java·数据结构·数据库·redis·后端·缓存
五敷有你4 小时前
Go:hello world
开发语言·后端·golang