分片篇: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 BY和ORDER 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 条数据,应该是 95 和 90。

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

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

正确的做法是将分页条件改写为 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 进行改写来防止将多余的数据写入到数据库中。


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

相关推荐
步行cgn1 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈2 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780512 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123452 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒3 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记3 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598783 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端
雨中飘荡的记忆3 小时前
Claude Code 入门必会:@、!、Slash Commands,一篇讲清楚
人工智能·后端
用户8356290780513 小时前
如何使用 Python 在 Word 文档中创建和格式化列表
后端·python
开心就好20253 小时前
uni-app 本地打包 iOS,在 Windows 上使用 KXBuild 产出 ipa 并上架的完整流程
后端·ios