关于java:ShardingSphereJDBC分片解析引擎

30次阅读

共计 9828 个字符,预计需要花费 25 分钟才能阅读完成。

前言

上文 ShardingSphere-JDBC 入门实战中对 ShardingSphere-JDBC 如何应用做了简略介绍,接下来打算从源码层面对数据分片做更加具体的介绍,整个数据分片会通过一个简单的流程包含:解析、路由、改写、执行、归并这几个子流程,每个子流程都有对应的引擎来解决,本文重点剖析子流程中的解析引擎。

分片流程

在介绍解析引擎之前,咱们对各个子流程做一个简略的介绍;咱们能够设想一下大略要通过几个流程;首先用户操作的都是逻辑表,最终是要被替换成物理表的,所以须要对 SQL 进行解析,其实就是了解 SQL;而后就是依据分片路由算法,应该路由到哪个表哪个库;接下来须要生成实在的 SQL,这样 SQL 能力被执行;生成的 SQL 可能有多条,每条都要执行;最初把多条执行的后果进行归并,返回后果集;整个流程大抵如下(来自官网):

SQL 解析 => 执行器优化 => SQL 路由 => SQL 改写 => SQL 执行 => 后果归并 的流程组成;每个子流程都有专门的引擎:

  • SQL 解析:分为词法解析和语法解析。先通过词法解析器将 SQL 拆分为一个个不可再分的单词。再应用语法解析器对 SQL 进行了解,并最终提炼出解析上下文。解析上下文包含表、选择项、排序项、分组项、聚合函数、分页信息、查问条件以及可能须要批改的占位符的标记;
  • 执行器优化:合并和优化分片条件,如 OR 等;
  • SQL 路由:依据解析上下文匹配用户配置的分片策略,并生成路由门路;目前反对分片路由和播送路由;
  • SQL 改写:将 SQL 改写为在实在数据库中能够正确执行的语句。SQL 改写分为正确性改写和优化改写;
  • SQL 执行:通过多线程执行器异步执行;
  • 后果归并:将多个执行后果集归并以便于通过对立的 JDBC 接口输入。后果归并包含流式归并、内存归并和应用装璜者模式的追加归并这几种形式。

本文重点剖析 SQL 解析局部,然而在剖析之前咱们须要大抵理解其中的 ANTLR 外围组件;

对于 ANTLR

ANTLR (Another Tool for Language Recognition) 是一个弱小的解析器的生成器,能够用来读取、解决、执行或翻译结构化文本或二进制文件。他被宽泛用来构建语言,工具和框架。ANTLR 能够从语法上来生成一个能够构建和遍历解析树的解析器。

ANTLR 官网地址:https://www.antlr.org

ANTLR 由两局部组成:

  • 将用户自定义语法翻译成 Java 中的解析器 / 词法分析器的工具,对应 antlr-complete.jar;
  • 解析器运行时须要的环境库文件,对应 antlr-runtime.jar;

ANTLR 语法

ANTLR 默认是一个已.g4 结尾的文件,一个语法定义文件一般来说有一个通用的构造如下:

/** Optional javadoc style comment */ 
grammar Name; ① 
options {...} 
import ... ; 

tokens {...} 
channels {...} // lexer only 
@actionName {...} 

rule1 // parser and lexer rules, possibly intermingled 
... 
ruleN
  • grammar:语法名称,必须和文件名统一;能够蕴含前缀 lexer 和 parser,如下所示:

    lexer grammar MySqlLexer;
    parser grammar MySqlParser;
  • options:能够在语法和规定元素级别指定许多选项,grammar 能够蕴含:superClass、language、tokenVocab、TokenLabelType、contextSuperClass 等,比方

    options {tokenVocab=MySqlLexer;}
  • import:将一个语法宰割成多个逻辑上的、可复用的块,有点相似超类;
  • tokens:为那些没有关联词法规则的 grammar 来定义 tokens 的类型;

    // explicitly define keyword token types to avoid implicit definition warnings
    tokens {BEGIN, END, IF, THEN, WHILE}
     
    @lexer::members { // keywords map used in lexer to assign token types
    Map<String,Integer> keywords = new HashMap<String,Integer>() {{put("begin", KeywordsParser.BEGIN);
        put("end", KeywordsParser.END);
        ...
    }};
    }
  • channels:只有 lexer(词法剖析)的 grammar 能力蕴含自定义的channels,比方:

    channels {
      WHITESPACE_CHANNEL,
      COMMENTS_CHANNEL
    }

以上 channels 能够在 lexer(词法剖析)规定中像枚举一样应用:

WS : [\r\t\n]+ -> channel(WHITESPACE_CHANNEL) ;
  • actionName:目前只有两个定义的命名操作(针对 Java 指标)在语法规定之外应用:headermembers;前者在识别器类定义之前将代码注入到生成的识别器类文件中,后者将代码作为字段和办法注入到识别器类定义中。
  • rule:规定能够分为:Lexer Rules 和 Parser Rules;规定格局如下所示:

    ​```
    ruleName : alternative1 | ... | alternativeN ;
    ​```

Lexer Rules:名称以大写字母结尾;

Parser Rules:名称以小写字母结尾;

更多参考官网文档:https://github.com/antlr/antlr4/blob/master/doc/index.md

ANTLR 应用

配置环境

首先须要去官网下载 antlr-complete.jar 文件,我这里应用的版本是:4.7.2;而后须要配置CLASSPATH

.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar;E:\antlr\antlr-4.7.2-complete.jar

检测一下是否胜利:

E:\antlr>java org.antlr.v4.Tool
ANTLR Parser Generator  Version 4.7.2
 -o ___              specify output directory where all output is generated
 -lib ___            specify location of grammars, tokens files
 -atn                generate rule augmented transition network diagrams
 ......

语法文件

咱们须要依据 ANTLR 提供的语法定义本人的语法文件,比方 Hello.g4 如下所示:

// Define a grammar called Hello
grammar Hello;
r  : 'hello' ID ;         // match keyword hello followed by an identifier
ID : [a-z]+ ;             // match lower-case identifiers
WS : [\t\r\n]+ -> skip ; // skip spaces, tabs, newlines

解决语法文件

应用 ANTLR 执行如下命令:

E:\antlr>java -jar antlr-4.7.2-complete.jar Hello.g4

会在当前目录下生成如下一堆文件:

HelloParser.java
HelloLexer.java
HelloListener.java
HelloBaseListener.java
HelloLexer.tokens
Hello.tokens
HelloLexer.interp
Hello.interp

测试

首先须要编译下面生成的 java 类:

E:\antlr>javac Hello*.java

通过如下命令,展现树形图形:

E:\antlr>java org.antlr.v4.gui.TestRig Hello  r -gui
hello zhaohui
^Z

注:最初的结尾 unix 应用 ctrl+D,windows 应用 ctrl+Z;

插件形式

除了以上形式还能够间接在 IDE 中应用插件,各种 IDE 的插件地址能够间接在官网查看:

插件地址:https://www.antlr.org/tools.html

解决语法文件

在 Hello.g4 文件上右击“Configure Antlr…”,如下所示:

其中几个比拟重要的配置包含:生成文件输入的地位、生成类指定的包名、语法树遍历的模式;

语法树遍历的模式其中能够配置两种模式:listener 模式和 visitor 模式

测试

同样应用 Hello.g4 语法文件,在 IDEA 中,关上 Hello.g4 右击 ”Test Rule”,ANTLR 视图显示如下:

代码实现

有了以上的测试就能够应用代码来获取 Parse tree,进行遍历;看上面一个简略的实例:

public class HelloDemo {public static void main(String[] args) {CharStream input = CharStreams.fromString("hello zhaohui");
        HelloLexer lexer = new HelloLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        HelloParser parser = new HelloParser(tokens);
        ParseTree tree = parser.r();
        System.out.println(tree.toStringTree(parser));
    }
}

输入后果如下:

(r hello zhaohui)

解析引擎

解析过程分为词法解析和语法解析。词法解析器用于将 SQL 拆解为不可再分的原子符号,称为 Token。并依据不同数据库方言所提供的字典,将其归类为关键字,表达式,字面量和操作符。再应用语法解析器将词法解析器的输入转换为形象语法树。

从 3.0.x 版本开始,应用 ANTLR 来做词法解析器,每种反对的数据库都有本人的方言,针对每种数据库都有各自的解析器;通过下面的理解咱们能够通过 ANTLR 来主动生成须要的解析器,前提是咱们有 Lexer 和 Parser 文件;

语法文件

ANTLR 在 Github 上提供了各种数据的语法文件,门路如下:

文件门路:https://github.com/antlr/grammars-v4/tree/master/sql

以 Mysql 为例,蕴含了两个文件:

MySqlLexer.g4
MySqlParser.g4

这样就能够通过相干工具生成须要的解析类了,在 shardingsphere-sql-parser-mysql 中能够发现主动生成类(autogen):

当然咱们也能够在 IDEA 中做一个简略的测试,输出一条常见的查问 SQL:

SELECT * FROM ORDER WHERE USER_ID=111;

生成的树结构如下所示:

解析引擎

ShardingSphere-JDBC 提供的解析引擎类为:SQLParserEngine,次要的一个外围办法如下:

private SQLStatement parse0(final String sql, final boolean useCache) {if (useCache) {Optional<SQLStatement> cachedSQLStatement = cache.getSQLStatement(sql);
            if (cachedSQLStatement.isPresent()) {return cachedSQLStatement.get();
            }
        }
        ParseTree parseTree = new SQLParserExecutor(databaseTypeName, sql).execute().getRootNode();
        SQLStatement result = (SQLStatement) ParseTreeVisitorFactory.newInstance(databaseTypeName, VisitorRule.valueOf(parseTree.getClass())).visit(parseTree);
        if (useCache) {cache.put(sql, result);
        }
        return result;
    }

两个参数别离是:逻辑 SQL、是否应用缓存;返回值为 SQLStatement;首先会进行是否应用缓存的判断,接下来就是要害的两步:逻辑 SQL 转换为 ParseTree、拜访 ParseTree 获取 SQLStatement;

转换 ParseTree

要转换 SQL 为 ParseTree,首先须要获取 Parser,而获取 Parser 须要获取 Lexer,写法其实和下面的 HelloDemo 差不多:

private static SQLParser createSQLParser(final String sql, final SQLParserConfiguration configuration) {Lexer lexer = (Lexer) configuration.getLexerClass().getConstructor(CharStream.class).newInstance(CharStreams.fromString(sql));
        return configuration.getParserClass().getConstructor(TokenStream.class).newInstance(new CommonTokenStream(lexer));
    }

不同的数据类型会获取不同的 Lexer 和 SQLParser;ShardingSphere-JDBC 提供了多种数据库反对;

  • Lexer:MySQLLexerOracleLexerPostgreSQLLexerSQL92LexerSQLServerLexer
  • SQLParser:MySqlParserOracleParserPostgreSQLParserSQL92ParserSQLServerParser

以上类其实都是对主动生成类的包装,以 MysqlParser 为例:

public final class MySQLParser extends MySQLStatementParser implements SQLParser {public MySQLParser(final TokenStream input) {super(input);
    }
    
    @Override
    public ASTNode parse() {return new ParseASTNode(execute());
    }
}

执行 MySQLParser 的 parser 办法,其实调用的是主动生成类 MySQLStatementParser 中的 execute 办法;

获取 SQLStatement

有了 ParseTree 接下来就须要遍历树获取 SQLStatement,ShardingSphere-JDBC 默认应用的遍历形式是 visitor 形式;通过 visitor 对形象语法树遍历结构域模型,通过域模型 (SQLStatement) 去提炼分片所需的上下文,并标记有可能须要改写的地位,同样每种数据库都要提供各自的visitor,目前反对的数据库包含:

visitor:MySQLVisitorOracleVisitorPostgreSQLVisitorSQL92VisitorSQLServerVisitor

SQLStatement

通过 visitor 生成对应的SQLStatement,不同的 SQL 生成的 SQLStatement 是不同的,大体能够分为这么几类:

  • DALStatement:全称Data Access Layer,数据库拜访层,包含 show databases、tables 等;
  • DMLStatement:全称Data Manipulation Language,数据库操作语言,包含增删改查等;
  • DCLStatement:全称Data Control Language,数据库管制语言,包含受权,传授管制等;
  • DDLStatement:全称Data Definition Language,数据库定义语言,包含创立、批改、删除表等;
  • RLStatement:全称Replication,包含主从复制等;
  • TCLStatement:全称Transaction Control Language,事务管制语言,包含设置保留点,回滚等;

关上对应数据库的语法文件,能够发现外面有对应的规定,如 MySqlParser:

sqlStatement
    : ddlStatement | dmlStatement | transactionStatement
    | replicationStatement | preparedStatement
    | administrationStatement | utilityStatement
    ;

以上每种类型都提供了本人的visitor

DALVisitor、DCLVisitor、DDLVisitor、DMLVisitor、RLVisitor、TCLVisitor

DMLStatement

以最常见的查问 SQL 为例,生成的是一个 DMLStatement,常见的子类有:

DMLStatement:CallStatementDeleteStatementDoStatementInsertStatementReplaceStatementSelectStatementUpdateStatement

对应的语法文件也有对应关系:

dmlStatement
    : selectStatement | insertStatement | updateStatement
    | deleteStatement | replaceStatement | callStatement
    | loadDataStatement | loadXmlStatement | doStatement
    | handlerStatement
    ;

以上每种操作类型都须要在对应的 Visitor 中进行重载,以 Mysql 为例对应的 DMLVisitor 为MySQLDMLVisitor,相干 select 语句的办法重载,访问者模式遍历之后生成 SelectStatement;

 @Override
    public ASTNode visitSelect(final SelectContext ctx) {
        // TODO :Unsupported for withClause.
        SelectStatement result = (SelectStatement) visit(ctx.unionClause());
        result.setParameterCount(getCurrentParameterIndex());
        return result;
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public ASTNode visitSelectClause(final SelectClauseContext ctx) {SelectStatement result = new SelectStatement();
        result.setProjections((ProjectionsSegment) visit(ctx.projections()));
        if (null != ctx.selectSpecification()) {result.getProjections().setDistinctRow(isDistinct(ctx));
        }
        if (null != ctx.fromClause()) {CollectionValue<TableReferenceSegment> tableReferences = (CollectionValue<TableReferenceSegment>) visit(ctx.fromClause());
            for (TableReferenceSegment each : tableReferences.getValue()) {result.getTableReferences().add(each);
            }
        }
        if (null != ctx.whereClause()) {result.setWhere((WhereSegment) visit(ctx.whereClause()));
        }
        if (null != ctx.groupByClause()) {result.setGroupBy((GroupBySegment) visit(ctx.groupByClause()));
        }
        if (null != ctx.orderByClause()) {result.setOrderBy((OrderBySegment) visit(ctx.orderByClause()));
        }
        if (null != ctx.limitClause()) {result.setLimit((LimitSegment) visit(ctx.limitClause()));
        }
        if (null != ctx.lockClause()) {result.setLock((LockSegment) visit(ctx.lockClause()));
        }
        return result;
    }
SelectStatement

查问 SQL 对应SelectStatement,局部代码如下:

public final class SelectStatement extends DMLStatement {
    
    private ProjectionsSegment projections;
    private final Collection<TableReferenceSegment> tableReferences = new LinkedList<>();
    private WhereSegment where;
    private GroupBySegment groupBy;
    private OrderBySegment orderBy;
    private LimitSegment limit;
    private SelectStatement parentStatement;
    private LockSegment lock;
}

能够发现外面蕴含了很多 Segment,每个Segment 其实就是整个 SQL 的一部分,下面这些关键字是不是都很相熟,都是在查问语句中会呈现的;其余类型这里就不贴代码了,依据每种类型生成各自的 Segment;最初将SQLStatement 包装成上下文 SQLStatementContext 给上游的路由引擎应用;

同样语法文件也有对应关系:

selectStatement
    : querySpecification lockClause?                                #simpleSelect
    | queryExpression lockClause?                                   #parenthesisSelect
    | querySpecificationNointo unionStatement+
        (UNION unionType=(ALL | DISTINCT)?
          (querySpecification | queryExpression)
        )?
        orderByClause? limitClause? lockClause?                     #unionSelect
    | queryExpressionNointo unionParenthesis+
        (UNION unionType=(ALL | DISTINCT)?
          queryExpression
        )?
        orderByClause? limitClause? lockClause?                     #unionParenthesisSelect
    ;

总结

本文重点介绍了整个分片流程中的解析流程,整个解析的外围就是 ANTLR,如果理解了 ANTLR 的相干语法,以及遍历形式,那解析引擎根本没什么难度了,ANTLR 官网文档还是比拟全面的,有趣味的能够去细读;下文持续剖析分片的路由机制。

参考

https://shardingsphere.apache…

感激关注

能够关注微信公众号「回滚吧代码」,第一工夫浏览,文章继续更新;专一 Java 源码、架构、算法和面试。

正文完
 0