百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 优雅编程 > 正文

轻量级数据库中间件Sharding-JDBC源码分析SQL 解析之更新SQL

sinye56 2024-10-21 10:58 5 浏览 0 评论

Sharding-JDBC架构图如下:

左边部分是部署架构图,右边部分则是核心逻辑架构图。

使用Sharding-JDBC,性能是大家最关心的问题。

关注、转发、评论头条号每天分享java知识,私信回复“555”赠送一些Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式资料

在数据量一致的情况下,使用Sharding-JDBC和原生JDBC的性能测试报告如下:

  • 查询操作:Sharding-JDBC的TPS为JDBC的TPS的99.8%。
  • 插入操作:Sharding-JDBC的TPS为JDBC的TPS的90.2%。
  • 更新操作:Sharding-JDBC的TPS为JDBC的TPS的93.1%。
  • 可以看到,Sharding-JDBC在查询中的性能损失非常低,插入和更新略高。

将单表的数据拆分为二,放入两个表中,使用Sharding-JDBC和原生JDBC的性能测试报告如下:

  • 查询操作:TPS双库比单库可以增加大约94%的性能。
  • 插入操作:TPS双库比单库可以增加大约60%的性能。
  • 更新操作:TPS双库比单库可以增加大约89%的性能。
  • 结果表明,Sharding-JDBC可有效利用水平扩展大幅度提升性能。

下面我将按照模块深度剖析Sharding-JDBC的详细功能和主要实现,请大家和我一起探索与评估它的水有多深。

分片规则配置

Sharding-JDBC的分片策略配置是自定义的,因此可以通过编程的方式最大限度的灵活调整。它并不仅支持=运算符分片,可支持BETWEEN和IN的运算符分片,支持将一条逻辑SQL最终散落至多个数据节点。同时支持多分片键,例如:根据用户ID分库,订单ID分表这种分库分表结合的分片策略;或根据年分库,月份+用户区域ID分表这样的多片键分片。

通过编程的方式定制分片规则虽然灵活,但配置起来略显繁琐。因此Sharding-JDBC又提供了Inline表达式编写分片策略的方式,用于配置集中化,以避免配置散落在配置文件和代码中的情况。此外,它还提供了定制化的Spring命名空间和YAML进一步简化配置。

JDBC规范重写

Sharding-JDBC对JDBC规范的重写思路是针对DataSource、Connection、Statement、PreparedStatement和ResultSet这5个核心接口封装,将多个实现类集合纳入Sharding-JDBC实现类管理。分布式主键也属于JDBC协议的一部分。

Sharding-JDBC尽量最大化实现JDBC协议,但分布式毕竟与原生JDBC不同,所以目前仍有未实现的接口,包括游标,存储过程、SavePoint以及向前遍历和修改ResultSet等不太常用的功能。此外,为了保证兼容性,并未实现JDBC 4.1及其后发布的接口(如:DBCP 1.x版本不支持JDBC 4.1)。

SQL解析

SQL解析作为分库分表类产品的核心,性能和兼容性是最重要的衡量指标。目前常见的SQL解析器主要有fdb,jsqlparser和Druid。Sharding-JDBC1.4.x之前的版本使用Druid作为SQL解析器,经实际测试,它的性能远超其它解析器。

从1.5.x版本开始,Sharding-JDBC采用完全自研的SQL解析引擎。由于目的不同,它并不需要将SQL转为AST语法树,也无需通过Visitor的方式二次遍历。它采用对SQL“半理解”的方式,仅提炼分片需要关注的上下文,因此SQL解析的性能和容错性得到了进一步的提高。

SQL解析模块由Lexer和Parser两个模块组成。Lexer用于将SQL拆解为Token,并将其归类为关键词,表达式,字面量和操作符。Parser则用于理解SQL和提炼分片上下文,并标记可能需要改写的位置。分片上下文包含SELECTItems、表信息、分片条件、自增主键信息、排序信息、分组信息和Limit信息。一次解析过程是不可逆的,一个个Token的依次解析,因此解析性能很高。由于各种数据库的SQL差异很大,因此在解析模块对每种数据库提供方言的支持。

Sharding-JDBC支持各种连接、聚合、排序、分组以及分页的解析,并且可以有限度的支持子查询。

SQL路由

SQL路由是根据分片规则配置以及解析上下文中的分片条件,将SQL定位至真正的数据源。它又分为直接路由、简单路由和笛卡尔积路由。

满足直接路由的条件比较苛刻,如果通过Hint(通过HintAPI直接指定路由至库表)方式分片,且仅分库,则无需SQL解析和结果归并。因此它的SQL兼容性最好,可以执行包括子查询、OR、UNION等复杂情况的任意SQL。

简单路由是Sharding-JDBC最推荐使用的分片方式,它是指不包含JOIN或仅包含Binding表JOIN的SQL。Binding表是指使用同样的分片键和分片规则的一组表,也就是说任何情况下,Binding表的分片结果应与主表一致。例如:order表和order_item表,都根据order_id分片,结果应是order_1与order_item_1成对出现。这样的关联查询和单表查询复杂度和性能相当。如果分片条件不是等于,而是BETWEEN或IN,则路由结果不一定落入单库(表),因此一条逻辑SQL最终可能拆分为多条SQL语句。

笛卡尔积查询最为复杂,因为无法根据Binding关系定位分片规则的一致性,所以非Binding表的关联查询需要拆解为笛卡尔积组合执行。查询性能较低,而且数据库连接数较高,需谨慎使用。

SQL改写

SQL改写模块的用途是将逻辑SQL改写为可以分布式执行的SQL。在Sharding-JDBC 1.5.x版本,SQL改写进行了调整和大量优化。1.4.x及之前版本,SQL改写是在SQL路由之前完成的,在1.5.x中调整为SQL路由之后,因为SQL改写可以根据路由至单库表还是多库表而进行进一步优化。SQL改写分为正确性改写和优化改写两部分。

正确性改写包括将分表的逻辑表名称替换为真实表名称,修正分页信息和增加补列。举两个例子:

  • AVG计算。分布式场景,以avg1 + avg2 + avg3 / 3计算平均值并不正确,需要改写为 (sum1 + sum2 + sum3) / (count1 + count2 + count3)。这就需要将包含AVG的SQL改写为SUM和COUNT,并在结果归并时重新计算平均值。
  • 分页。假设每10条数据为一页,取第2页数据。在分片环境下获取LIMIT 10, 10,归并之后再根据排序条件取出前10条数据是不正确的结果。正确的做法是将分条件改写为LIMIT 0, 20,取出所有前两页数据,再结合排序条件计算出正确的数据。因此越是获取靠后数据,分页的效率就会越低。有很多方法可避免使用LIMIT进行分页。比如构建记录行记录数和行偏移量的二级索引,或使用上次分页数据结尾ID作为下次查询条件的分页方式。

优化改写是1.5.x重点提升的部分,实现的功能比较零散,这里同样举两个例子:

  • 单路由拒绝改写。这是将SQL改写挪到SQL路由之后的原因。当获得路由结果之后,单路由的情况因为不涉及到结果归并,因此分页、补列等改写都无需存在。尤其是分页,无需将数据从第1条开始取,节省了网络带宽。
  • 流式归并改写。一会讲到归并时会说,这里先提一句,将仅包含GROUPBY的SQL改写为GROUPBY + ORDERBY。

SQL执行

路由至真实数据源后,Sharding -JDBC将采用多线程并发执行SQL。它用3种执行引擎分别对应处理Statement,PreparedStatement和AddBatchPreparedStatement。Sharding-JDBC线程池放在一个名为ShardingContext的对象中,它的生命周期同ShardingDataSource保持一致。如果一个应用中创建了多个Sharding-JDBC的数据源,它们将持有不同的线程池。

结果归并

Sharding-JDBC支持的结果归并从功能上分为遍历、排序、分组和分页4种类型,它们是组合而非互斥的关系。从结构划分,可分为流式归并、内存归并和装饰者归并。流式归并和内存归并是互斥的,装饰者归并可以在流式归并和内存归并之上做进一步的处理。

流式归并是将数据游标与结果集的游标保持一致,顺序的从结果集中一条条的获取正确的数据。遍历和排序都是流式归并,分组比较复杂,分为流式分组和内存分组。内存归并则是需要将结果集的所有数据都遍历并存储在内存中,再通过内存归并后,将内存中的数据伪装成结果集返回。

遍历类型最为简单,只需将多结果集组成链表,遍历完成当前结果集后,将链表位置后移,继续遍历下一个结果集即可。

排序类型稍微复杂,由于ORDER BY的原因,每个结果集自身数据是有序的,因此只需要将结果集当前游标指向的值排序即可。Sharding-JDBC在排序类型归并时,将每个结果集的当前排序数据实现了比较器,并将其放入优先级队列。每次JDBC调用next时,将队列顶端的结果集出队并next,然后获取新的队列顶端的结果集供JDBC获取数据。

分组类型最为复杂,分组归并已经不属于OLTP范畴,而更面向OLAP,但由于遗留系统使用很多,因此Sharding-JDBC还是将其实现。分组归并分成流式分组归并和内存分组归并。流式分组归并节省内存,但必须要求排序和分组的数据保持一致。如果GROUPBY和ORDER BY的内容不一致,则必须使用内存分组归并。由于数据不是按照分组需要的顺序取出,因此需要将结果集中的所有数据全部加载至内存。在SQL改写时提到的仅有GROUP BY的SQL,会优化增加ORDER BY语句,即使将内存分组归并优化为流式分组归并的提升。

无论是流式分组还是内存分组,对聚合的处理都是一致的。聚合分为比较、累加和平均值3种类型。比较聚合包括MAX和MIN,只返回最大(小)结果。累加聚合包括SUM和COUNT,需要将结果累加后返回。平均值聚合则是通过SQL改写的SUM和COUNT计算,相关内容已在SQL改写涵盖,不再赘述。

最后再聊一下装饰者归并,他是对所有的结果集归并进行统一的功能增强,目前装饰者归并只有分页一种类型。

上述的所有归并类型,都可能分页或不分页,因此可以通过装饰者模式来增加分页的能力。分页归并会将改写的LIMIT中,不需要获取的数据过滤掉。Sharding-JDBC的分页很容易产生误解,很多人认为分页会占用大量内存,因为Sharding-JDBC会因为分布式正确性的考量,将LIMIT 100000, 10改写为LIMIT 0, 100010,产生Sharding-JDBC会将100010数据都加载到内存的错觉。通过上面分析可知,会全部加载到内存的只有内存分组归并这一种情况。其他情况都是通过流式获取结果集数据的方式,因此Sharding-JDBC会通过结果集的next方法将无需取出的数据全部跳过,并不会将其存入内存。

分布式主键

分布式主键在这里单独提炼出一个章节,因为它是贯穿于Sharding-JDBC整个生命周期的。

分布式主键最独立的部分是生成策略,Sharding-JDBC提供灵活的配置分布式主键生成策略方式。在分片规则配置模块可配置每个表的主键生成策略,默认使用snowflake。

通过策略生成的分布式主键可以无缝的融入JDBC协议,它实现了Statement的getGeneratedKeys方法,将其返回改写后的Result和ResultMetaData,将Sharding-JDBC生成的分布式主键伪装为数据库生成的自增主键返回。

SQL解析时,需要根据分布式主键配置策略判断是否在逻辑SQL中已包含主键列,如果未包含则需要将INSERTItems和INSERT Values的最后位置写入解析上下文。

SQL改写时,将根据解析上下文中的位置改写SQL,增加未包含的主键列名称和值。如果是Statement则在INSERT Values后追加生成后的分布式主键;如果是PreparedStatement则在INSERT Values后追加?,并在传入的参数后追加生成后的分布式主键。

更新SQL解析比查询SQL解析复杂度低的多的多。不同数据库在插入SQL语法上也统一的多。本文分享 MySQL 更新SQL解析器 MySQLUpdateParser

MySQL UPDATE 语法一共有 2 种 :

  • 第一种:Single-table syntax
UPDATE [LOW_PRIORITY] [IGNORE] table_reference
 SET col_name1={expr1|DEFAULT} [, col_name2={expr2|DEFAULT}] ...
 [WHERE where_condition]
 [ORDER BY ...]
 [LIMIT row_count]
  • 第二种:Multiple-table syntax
UPDATE [LOW_PRIORITY] [IGNORE] table_references
 SET col_name1={expr1|DEFAULT} [, col_name2={expr2|DEFAULT}] ...
 [WHERE where_condition]

Sharding-JDBC 目前仅支持第一种。业务场景上使用第二种的很少很少。

Sharding-JDBC 更新SQL解析主流程如下:

// AbstractUpdateParser.java
@Override
public UpdateStatement parse() {
 sqlParser.getLexer().nextToken(); // 跳过 UPDATE
 skipBetweenUpdateAndTable(); // 跳过关键字,例如:MYSQL 里的 LOW_PRIORITY、IGNORE
 sqlParser.parseSingleTable(updateStatement); // 解析表
 parseSetItems(); // 解析 SET
 sqlParser.skipUntil(DefaultKeyword.WHERE);
 sqlParser.setParametersIndex(parametersIndex);
 sqlParser.parseWhere(updateStatement);
 return updateStatement; // 解析 WHERE
}

Sharding-JDBC 正在收集使用公司名单:传送门。

你的登记,会让更多人参与和使用 Sharding-JDBC。传送门

Sharding-JDBC 也会因此,能够覆盖更多的业务场景。传送门

登记吧,骚年!传送门

2. UpdateStatement

更新SQL 解析结果。

public final class UpdateStatement extends AbstractSQLStatement {
}

对,没有其他属性。

我们来看下 UPDATE t_user SET nickname = ?, age = ? WHERE user_id = ? 的解析结果

3. #parse()

3.1 #skipBetweenUpdateAndTable()

在 UPDATE 和 表名 之间有些词法,对 SQL 路由和改写无影响,进行跳过。

// MySQLUpdateParser.java
@Override
protected void skipBetweenUpdateAndTable() {
 getSqlParser().skipAll(MySQLKeyword.LOW_PRIORITY, MySQLKeyword.IGNORE);
}
// OracleUpdateParser.java
@Override
protected void skipBetweenUpdateAndTable() {
 getSqlParser().skipIfEqual(OracleKeyword.ONLY);
}

3.2 #parseSingleTable()

解析,请看《SQL 解析(二)之SQL解析》的 #parseSingleTable() 小节。

3.3 #parseSetItems()

解析SET后语句。

// AbstractUpdateParser.java
/**
* 解析多个 SET 项
*/
private void parseSetItems() {
 sqlParser.accept(DefaultKeyword.SET);
 do {
 parseSetItem();
 } while (sqlParser.skipIfEqual(Symbol.COMMA)); // 以 "," 分隔
}
/**
* 解析单个 SET 项
*/
private void parseSetItem() {
 parseSetColumn();
 sqlParser.skipIfEqual(Symbol.EQ, Symbol.COLON_EQ);
 parseSetValue();
}
/**
* 解析单个 SET 项
*/
private void parseSetColumn() {
 if (sqlParser.equalAny(Symbol.LEFT_PAREN)) {
 sqlParser.skipParentheses();
 return;
 }
 int beginPosition = sqlParser.getLexer().getCurrentToken().getEndPosition();
 String literals = sqlParser.getLexer().getCurrentToken().getLiterals();
 sqlParser.getLexer().nextToken();
 if (sqlParser.skipIfEqual(Symbol.DOT)) { // 字段有别名
 // TableToken
 if (updateStatement.getTables().getSingleTableName().equalsIgnoreCase(SQLUtil.getExactlyValue(literals))) {
 updateStatement.getSqlTokens().add(new TableToken(beginPosition - literals.length(), literals));
 }
 sqlParser.getLexer().nextToken();
 }
}
/**
* 解析单个 SET 值
*/
private void parseSetValue() {
 sqlParser.parseExpression(updateStatement);
 parametersIndex = sqlParser.getParametersIndex();
}

3.4 #parseWhere()

Sharding-JDBC将作为面向OLTP在线业务的分片化的数据库治理微服务基础组件积极的发展下去。真诚邀请感兴趣的人关注和参与。

关注、转发、评论头条号每天分享java知识,私信回复“555”赠送一些Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式资料

相关推荐

程序员:JDK的安装与配置(完整版)_jdk的安装方法

对于Java程序员来说,jdk是必不陌生的一个词。但怎么安装配置jdk,对新手来说确实头疼的一件事情。我这里以jdk10为例,详细的说明讲解了jdk的安装和配置,如果有不明白的小伙伴可以评论区留言哦下...

Linux中安装jdk并配置环境变量_linux jdk安装教程及环境变量配置

一、通过连接工具登录到Linux(我这里使用的Centos7.6版本)服务器连接工具有很多我就不一一介绍了今天使用比较常用的XShell工具登录成功如下:二、上传jdk安装包到Linux服务器jdk...

麒麟系统安装JAVA JDK教程_麒麟系统配置jdk

检查检查系统是否自带java在麒麟系统桌面空白处,右键“在终端打开”,打开shell对话框输入:java–version查看是否自带java及版本如图所示,系统自带OpenJDK,要先卸载自带JDK...

学习笔记-Linux JDK - 安装&配置

前提条件#检查是否存在JDKrpm-qa|grepjava#删除现存JDKyum-yremovejava*安装OracleJDK不分系统#进入安装文件目...

Linux新手入门系列:Linux下jdk安装配置

本系列文章是把作者刚接触和学习Linux时候的实操记录分享出来,内容主要包括Linux入门的一些理论概念知识、Web程序、mysql数据库的简单安装部署,希望能够帮到一些初学者,少走一些弯路。注意:L...

测试员必备:Linux下安装JDK 1.8你必须知道的那些事

1.简介在Oracle收购Sun后,Java的一系列产品就被整合到Oracle官网中,打开官网乍眼一看也不知道去哪里下载,还得一个一个的摸索尝试,而且网上大多数都是一些Oracle收购Sun前,或者就...

Linux 下安装JDK17_linux 安装jdk1.8 yum

一、安装环境操作系统:JDK版本:17二、安装步骤第一步:下载安装包下载Linux环境下的jdk1.8,请去官网(https://www.oracle.com/java/technologies/do...

在Ubuntu系统中安装JDK 17并配置环境变量教程

在Ubuntu系统上安装JDK17并配置环境变量是Java开发环境搭建的重要步骤。JDK17是Oracle提供的长期支持版本,广泛用于开发Java应用程序。以下是详细的步骤,帮助你在Ubuntu系...

如何在 Linux 上安装 Java_linux安装java的步骤

在桌面上拥抱Java应用程序,然后在所有桌面上运行它们。--SethKenlon(作者)无论你运行的是哪种操作系统,通常都有几种安装应用程序的方法。有时你可能会在应用程序商店中找到一个应用程序...

Windows和Linux环境下的JDK安装教程

JavaDevelopmentKit(简称JDK),是Java开发的核心工具包,提供了Java应用程序的编译、运行和开发所需的各类工具和类库。它包括了JRE(JavaRuntimeEnviro...

linux安装jdk_linux安装jdk软连接

JDK是啥就不用多介绍了哈,外行的人也不会进来看我的博文。依然记得读大学那会,第一次实验课就是在机房安装jdk,编写HelloWorld程序。时光飞逝啊,一下过了十多年了,挣了不少钱,买了跑车,娶了富...

linux安装jdk,全局配置,不同用户不同jdk

jdk1.8安装包链接:https://pan.baidu.com/s/14qBrh6ZpLK04QS8ogCepwg提取码:09zs上传文件解压tar-zxvfjdk-8u152-linux-...

运维大神教你在linux下安装jdk8_linux安装jdk1.7

1.到官网下载适合自己机器的版本。楼主下载的是jdk-8u66-linux-i586.tar.gzhttp://www.oracle.com/technetwork/java/javase/downl...

window和linux安装JDK1.8_linux 安装jdk1.8.tar

Windows安装JDK1.8的步骤:步骤1:下载JDK打开浏览器,找到JDK下载页面https://d.injdk.cn/download/oraclejdk/8在页面中找到并点击“下载...

最全的linux下安装JavaJDK的教程(图文详解)不会安装你来打我?

默认已经有了linux服务器,且有root账号首先检查一下是否已经安装过java的jdk任意位置输入命令:whichjava像我这个已经安装过了,就会提示在哪个位置,你的肯定是找不到。一般我们在...

取消回复欢迎 发表评论: