阅读 96

sharding-JDBC源码分析(二)SQL解析

SQL parser

SQL解析是根据语法与词法分析SQL,理解SQL含义,才能按照SQL语义处理数据,SQL解析是实现分库分表组件最基础的功能,熟悉Mysql架构的,内部也有很重要的一个模块就是SQL parser。

Sharding-JDBC目前SQL解析采用的是ANTLR解析器,先前1.x版本是采用的是Druid SQL解析,个人觉得druid SQL解析还是比较容易上手,分库分表中间件Mycat、Dble内部解析都是基于Druid实现的。Druid官方称解析性能比ANTLR这类解析器高10倍。至于sharding为什么还要使用ANTLR解析器,我觉得最主要的因素和灵活性有关吧。

有几个概念需要温习下

  • AST:在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。
  • 词法分析(Lexer):将语句拆分为一个个最小的词法单元(Tokens)比如常见的“>” 、 "="、“in” 等。词法分析只负责语句的拆分,剩下的就交给语法分析了。词法中主要包含以下几部分:关键字、常量、标识符、运算符和分界符。
  • 语法分析(parser):根据词法解析的结果,使用某种算法对词法解析的词法单元进行分析,生成抽象语法树(AST),外部可以遍历语法树,获取需要的内容。

举个栗子,以简单的select 语句为例 SQL语句主要由关键字、常量、标识符、运算符和分界符这五部分组成,根据词法分析的结果进行语法分析,首先有关键字select 说明这是一个查询语句, items中有 “id”和“age” 说明我们查询返回的列是这两个,依次类推,最后彻底理解SQL的含义。

ANTLR

什么是ANTLR?

ANTLR(另一种语言识别工具)是功能强大的解析器生成器,用于读取,处理,执行或翻译结构化文本或二进制文件。它被广泛用于构建语言,工具和框架。ANTLR通过语法生成可以构建和遍历语法树的语法分析器。(创始者特伦斯·帕尔(**Terence Parr)**是ANTLR背后的疯子,自1989年以来一直致力于语言工具。计算机科学教授)。

与Druid sql parser最大的区别就是ANTLR不是针对SQL解析而生,而Druid sql parser是为SQL解析而生,提供一站式SQL解析服务,无需使用者这定义规则,也许你会问那各种数据库的方言怎么解析?这点Druid的作者想到了,为各种数据库适配了SQL解析,而使用ANTLR,则需要使用者自己定义识别规则。各有所长吧。

  • 以官方hello为例,识别“hello”字符串,首先粘贴官网.g4文件
// Define a grammar called Hello
grammar Hello;
root  : 'hello' ID ;         // match keyword hello followed by an identifier
ID : [a-z|0-9]+ ;             // match lower-case identifiers
WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines
复制代码

解析字符串 hello 1001

  • 在看一个稍微复杂一点的对运算表达式的解析,.g4文件如下
grammar Expr;

@header {
package tools;
import java.util.*;
}

@parser::members {
    /** "memory" for our calculator; variable/value pairs go here */
    Map<String, Integer> memory = new HashMap<String, Integer>();

    int eval(int left, int op, int right) {
        switch ( op ) {
            case MUL : return left * right;
            case DIV : return left / right;
            case ADD : return left + right;
            case SUB : return left - right;
        }
        return 0;
    }
}

stat:   e NEWLINE           {System.out.println($e.v);}
    |   ID '=' e NEWLINE    {memory.put($ID.text, $e.v);}
    |   NEWLINE                   
    ;

e returns [int v]
    : a=e op=('*'|'/') b=e  {$v = eval($a.v, $op.type, $b.v);}
    | a=e op=('+'|'-') b=e  {$v = eval($a.v, $op.type, $b.v);}  
    | INT                   {$v = $INT.int;}    
    | ID
      {
      String id = $ID.text;
      $v = memory.containsKey(id) ? memory.get(id) : 0;
      }
    | '(' e ')'             {$v = $e.v;}       
    ; 

MUL : '*' ;
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;

ID  :   [a-zA-Z]+ ;      // match identifiers
INT :   [0-9]+ ;         // match integers
NEWLINE:'\r'? '\n' ;     // return newlines to parser (is end-statement signal)
WS  :   [ \t]+ -> skip ; // toss out whitespace
复制代码

在IDEA antlr4插件中解析如下表达式

1*(10+22)-33+80
复制代码

我们运行下sharding-JDBC中对select 解析,让大家有个整体印象

select id ,name from t_order where id=1001 or name ='qiqsa'
复制代码

解析结果

关于ANTLR更多知识,可以参考官网资料,而且提供了很多examples

Sharding-JDBC ANTLR

Sharding-JDBC使用了ANTLR定义了各种SQL解析规则,就是项目中 .g4文件

有兴趣的可以研究,本人觉得还是比较复杂,可能是由于对ANTLR语法还不是很熟悉吧,这里对这些规则就不做分析了。

Source code

直奔主题吧(在路由前调用的解析)sharding-JDBC内部提供了SQL解析引擎SQLParseEngine,看下入口

    public SQLStatement parse(final String sql, final boolean useCache) {
       //内部实现钩子方法,植入其他逻辑,主要作用是链路监控
        ParsingHook parsingHook = new SPIParsingHook();
        parsingHook.start(sql);
        try {
           //解析入口
            SQLStatement result = parse0(sql, useCache);
            parsingHook.finishSuccess(result);
            return result;
            // CHECKSTYLE:OFF
        } catch (final Exception ex) {
            // CHECKSTYLE:ON
            parsingHook.finishFailure(ex);
            throw ex;
        }
    }
复制代码

钩子方法在上篇分享sharding-JDBC源码分析(一)标准JDBC接口实现 已经提到过了,接续分析parse0方法

    private SQLStatement parse0(final String sql, final boolean useCache) {
      //根据是否使用缓存获取解析结果
        if (useCache) {
            Optional<SQLStatement> cachedSQLStatement = cache.getSQLStatement(sql);
            if (cachedSQLStatement.isPresent()) {
                return cachedSQLStatement.get();
            }
        }
      //这块是对SQL进行解析,并将解析结果遍历封装在SQLStatement对象中
        SQLStatement result = new SQLParseKernel(ParseRuleRegistry.getInstance(), databaseType, sql).parse();
        if (useCache) {
            cache.put(sql, result);
        }
        return result;
    }
复制代码

这块根据开关对解析结果使用了缓存,个人觉得对于缓存应该分为预编译和非预编译,对于非预编译其实没必要缓存,不过从另一方面来看,现在大部分ORM框架,比如mybatis使用基本都是预编译,非预编译很少

SQLParseKernel中的parse方法

    public SQLStatement parse() {
       //解析获取抽象语法树AST
        SQLAST ast = parserEngine.parse();
       //从语法树中获取sql 片段,稍后解释sql片段是什么意思
        Collection<SQLSegment> sqlSegments = extractorEngine.extract(ast);
        Map<ParserRuleContext, Integer> parameterMarkerIndexes = ast.getParameterMarkerIndexes();
       //将解析获取的语法树遍历填充在SQLStatement对象中返回,供后续路由使用
        return fillerEngine.fill(sqlSegments, parameterMarkerIndexes.size(), ast.getSqlStatementRule());
    }
复制代码

上边提到的sql片段即SQLSegment,其实是定义的一种数据结构,是对词法解析中每一个Token的封装,SQLSegment是接口,分析其中一个实现就明白怎么回事了

public class ColumnSegment implements SQLSegment, PredicateRightValue, OwnerAvailable<TableSegment> {
    //在sql中开始index
    private final int startIndex;
    //结束index
    private final int stopIndex;
    //字段名column
    private final String name;
    //是否有 `,’等, 比如mysql中查询中 select `id`,`name` from xxx
    private final QuoteCharacter quoteCharacter;
    //字段属于哪个表
    private TableSegment owner;
复制代码

现在明白SQLSegment怎么回事了吧,可以看下解析结果,更清晰

select `id` from t_order where `id` = 1001
复制代码

有了抽象语法树,需要将抽象语法树封装成我们需要的数据结构

    public SQLStatement fill(final Collection<SQLSegment> sqlSegments, final int parameterMarkerCount, final SQLStatementRule rule) {
       //根据语法规则中配置的class name获取实现类,比如有SQLSelectStatement 、SQLinsertStatement等
        SQLStatement result = rule.getSqlStatementClass().newInstance();
        Preconditions.checkArgument(result instanceof AbstractSQLStatement, "%s must extends AbstractSQLStatement", result.getClass().getName());
        ((AbstractSQLStatement) result).setParametersCount(parameterMarkerCount);
        result.getAllSQLSegments().addAll(sqlSegments);
       //对应的SQLSegment实现了filter,fiter作用是将有关键的SQLSegment放在对应的数据结构中,比如有orderby 或者 
        for (SQLSegment each : sqlSegments) {
            Optional<SQLSegmentFiller> filler = parseRuleRegistry.findSQLSegmentFiller(databaseType, each.getClass());
            if (filler.isPresent()) {
                filler.get().fill(each, result);
            }
        }
        return result;
    }
复制代码
    <filler-rule sql-segment-class="org.apache.shardingsphere.core.parse.sql.segment.dml.order.GroupBySegment" filler-class="org.apache.shardingsphere.core.parse.core.filler.impl.dml.GroupByFiller" />
    <filler-rule sql-segment-class="org.apache.shardingsphere.core.parse.sql.segment.dml.order.OrderBySegment" filler-class="org.apache.shardingsphere.core.parse.core.filler.impl.dml.OrderByFiller" />
    <filler-rule sql-segment-class="org.apache.shardingsphere.core.parse.sql.segment.dml.column.InsertColumnsSegment" filler-class="org.apache.shardingsphere.core.parse.core.filler.impl.dml.InsertColumnsFiller" />
    <filler-rule sql-segment-class="org.apache.shardingsphere.core.parse.sql.segment.dml.assignment.InsertValuesSegment" filler-class="org.apache.shardingsphere.core.parse.core.filler.impl.dml.InsertValuesFiller" />
    <filler-rule sql-segment-class="org.apache.shardingsphere.core.parse.sql.segment.dml.assignment.SetAssignmentsSegment" filler-class="org.apache.shardingsphere.core.parse.core.filler.impl.dml.SetAssignmentsFiller" />

复制代码

看下其中一个filter实现

public final class OrderByFiller implements SQLSegmentFiller<OrderBySegment> {
    
    @Override
    public void fill(final OrderBySegment sqlSegment, final SQLStatement sqlStatement) {
        ((SelectStatement) sqlStatement).setOrderBy(sqlSegment);
    }
}

复制代码

将orderBySegment保存在了SQLStatement中,就是这个作用

解析到这块基本结束,文中并没有说怎么从抽象语法树AST中提取解析结果,ANTLRT提供了两种实现方式,有兴趣的可以去查阅,我觉得和Druid的相似吧,druid中提供一种手写遍历语法树,用户自己实现,另外一种是基于visitor访问者模式实现的遍历。

对于上边提到的SQL最终解析结果如下,其中包含tableswhere等,看到这块应该理解了SQL解析的意义。

解析结束后,到了路由阶段

    public SQLRouteResult route(final String logicSQL) {
      //解析获得结果
        SQLStatement sqlStatement = shardingRouter.parse(logicSQL, false);
      //根据解析结果sqlStatement进行路由,路由会用到解析中的条件或者insert语句中的分片键
        return masterSlaveRouter.route(shardingRouter.route(logicSQL, Collections.emptyList(), sqlStatement));
    }
复制代码

结束语

本文主要简单的介绍了什么是ANTLR4,如果你准备用ANTLR,看这些远远不够的,网上这方面资料很全,官网也有很多demo,对sharding-JDBC中解析流程做了分析,读者可以按照这个思路来看源码;如果你对ANTLR不怎么熟悉,那么可以选择Druid sql parser作为SQL解析,个人认为还是算比较好上手,毕竟不用自己定义语法规则,本人对druid sql parser还算熟悉,有问题可以共同探讨,下篇对sharding-JDBC路由模块做分析。

关注下面的标签,发现更多相似文章
评论