Mycat源码篇 : Mycat sql解析模块分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/d6619309/article/details/52743525

mycat sql解析模块是mycat实现sql路由模块和sql结果集后处理模块的基础,在mycat的几大模块里面占据相当重要的位置。这篇文章首先简单介绍mycat里面sql解析模块的作用,后面结合部分源码来看这个模块的功能。

本文分析的mycat版本为1.6。假如你使用的是1.4或者1.5,也没关系,因为sql解析功能在1.4和更高版本在大方向上是没怎么变化的。在这里,我们只分析mycat服务(server)端口的sql解析。对于mycat管理(manager)端口的sql解析,不在本文讨论范围内。

1. sql解析模块功能说明

mycat作为一个分布式数据库中间件,其sql解析主要有以下作用:

(1) 判断sql语法是否正确

sql解析模块的一个重要功能就是判断sql是否符合语法要求。因为后端数据库默认对接mysql,所以在sql解析模块默认判断是否符合mysql语法。

(2) sql语句分类并对不同类型的sql语句做不同的逻辑处理

不同的逻辑处理包括决定sql语句是否被支持、sql拦截、sql缓存等。

mycat作为一款中间件,并非所有的mysql语法都支持,因此,对于一些特殊的不支持语法,在sql解析模块解析通过之后需要进行额外的判断, 然后直接返回响应(不支持此语法)给客户端。

(3) 为sql路由模块处理提供服务

sql路由模块是实现mycat分库分表的关键,而sql解析模块是sql路由模块实现的关键。sql路由依赖sql解析结果。举例说明,假设表customer按主键id取模分到2个节点上,其中id % 2 == 0位于dn1, id % 2 == 1 位于dn2,那么对于下面的语句:

select * from customer where id = 1;

经过sql解析模块可以得到where条件id = 1, 在sql路由模块中我们判断到id是分片字段,结合这个表的路由规则判断该语句可以直接发送到dn2。

(4) 为sql结果集后处理模块提供服务

当查询语句需要路由到多个节点的时候,mycat收到的结果集有多个,这个时候mycat需要判断结果集是否需要进行后处理,这是sql结果集后处理模块的工作。那么,什么样的条件下需要进行后处理呢?比如带group by、order by、limit等条件,又或者是使用count、sum、avg函数的时候,就需要启动结果集后处理。这些条件正是在sql解析模块中能够得到的。也正是因为在sql解析模块中能够得到这些信息,才使得sql结果集后处理模块的实现成为可能。

mycat的sql解析可以化分为两部分,一部分是浅解析,另外一部分是深解析。下面分别说明这两部分:

1.1 浅解析

浅解析负责得到sql语句类型,比如SELECT类型、DELETE类型、UPDATE类型、INSERT类型。对sql语句做类型的解析主要是为了能对不同类型的sql语句进行不同的逻辑处理,比如,对于SELECT语句会考虑是否缓存,是否利用缓存的路由结果;对于DELETE、UPDATE、INSERT语句,会判断权限,有权限才给执行,对于一些特殊语句,决定是否可以直接返回响应给客户端而不需要走到后端数据节点。

mycat的sql缓存是缓存sql对应的路由结果,而且只缓存SELECT类型的sql语句。

1.2 深解析

深解析需要解析整个sql语句,得到sql语法树(AST),比如下面的语句:

select name from user where id = 1;

经过深解析以后,我们能够知道select的具体列有哪些,from子句涉及的表名,where条件又有哪些。更复杂的还有解析子查询、group by、order by、limit、函数等等。

深解析的目的:

(1) 第一个目的是判断sql语法是否正确。深解析不仅仅涉及词法分析,更涉及语法分析,因此,它能够判断传进来的sql语句在语法上是否正确。

(2) 另外,可以为后面的sql路由模块服务,根据得到的sql解析树,在sql路由模块中得到准确的路由结果(这个语句应该发到哪些节点,有可能发单个节点,有可能发多个节点,有可能发所有的节点)。

(3) 最后,为sql结果集后处理模块服务,根据前面提到的,经过深解析得到的sql解析树带有整个sql语句的详细信息,里面的一些信息(比如order by、limit等)将作为sql结果集后处理的依据。

深解析涉及到sql词法分析、语法分析,在mycat中,利用了alibaba的druid sql parser来实现深解析。

druid是阿里开源出来的项目,代码托管在github上。sql parser只是它里面的一个附加功能,它的主要功能是用来做数据库连接池,类比c3p0。mycat1.3版本默认用的sql解析库是fdb parser,从1.4版本开始,基于sql解析器性能的考虑,换用druid的sql parser解析库,据说druid的解析库性能在sql足够长、足够复杂的情况下,是fdb parser的几十倍!

2. sql解析模块源码分析

2.1 浅解析

前面功能介绍部分提到,sql解析模块中的浅解析是为了解析出sql语句类型,然后根据不同的sql语句类型,做不同的逻辑处理。在mycat代码里面,sql浅解析是在ServerQueryHandler类的query方法里被调用,sql浅解析主要体现在通过ServerParse类的parse方法(静态方法)来得到sql语句类型。关注下这两个类相应代码:

(1) ServerQueryHandler的query方法会调用ServerParse的parse方法:


    @Override
    public void query(String sql) {

        ServerConnection c = this.source;
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(new StringBuilder().append(c).append(sql).toString());
        }
        //
        int rs = ServerParse.parse(sql);
        int sqlType = rs & 0xff;

        switch (sqlType) {
        //explain sql
        case ServerParse.EXPLAIN:
            ExplainHandler.handle(sql, c, rs >>> 8);
            break;
        //explain2 datanode=? sql=?
        case ServerParse.EXPLAIN2:
            Explain2Handler.handle(sql, c, rs >>> 8);
            break;
        case ServerParse.SET:
            SetHandler.handle(sql, c, rs >>> 8);
            break;
        case ServerParse.SHOW:
            ShowHandler.handle(sql, c, rs >>> 8);
            break;
        case ServerParse.SELECT:
            SelectHandler.handle(sql, c, rs >>> 8);
            break;
        // ... 其他case分支
        default:
            if(readOnly){
                LOGGER.warn(new StringBuilder().append("User readonly:").append(sql).toString());
                c.writeErrMessage(ErrorCode.ER_USER_READ_ONLY, "User readonly");
                break;
            }
            c.execute(sql, rs & 0xff);
        }
    }

(2) 在ServerParse的parse方法里面,逐个字符判断sql语句的第一个单词,得到不同的sql类型标识,标识请看这个类的静态final常量定义:


    public static final int OTHER = -1;
    public static final int BEGIN = 1;
    public static final int COMMIT = 2;
    public static final int DELETE = 3;
    public static final int INSERT = 4;
    public static final int REPLACE = 5;
    public static final int ROLLBACK = 6;
    public static final int SELECT = 7;
    public static final int SET = 8;
    public static final int SHOW = 9;
    public static final int START = 10;
    public static final int UPDATE = 11;
    public static final int KILL = 12;
    public static final int SAVEPOINT = 13;
    public static final int USE = 14;
    public static final int EXPLAIN = 15;
    public static final int EXPLAIN2 = 151;
    public static final int KILL_QUERY = 16;
    public static final int HELP = 17;
    public static final int MYSQL_CMD_COMMENT = 18;
    public static final int MYSQL_COMMENT = 19;
    public static final int CALL = 20;
    public static final int DESCRIBE = 21;
    public static final int LOCK = 22;
    public static final int UNLOCK = 23;
    public static final int LOAD_DATA_INFILE_SQL = 99;
    public static final int DDL = 100;

用int数值来表示不同的sql类型,如SELECT是用数字7表示。

(3) 然后根据解析到的sql类型,进入不同的逻辑分支,这体现在ServerQueryHandler类的switch代码段。对于特定的逻辑处理,封装到特定的Handler类里面进行处理,比如sql类型为SET的,使用SetHandler类进行处理,对于SELECT类型,使用SelectHandler类进行处理。

我们举SELECT类型来做说明,mysql常用的SELECT开头的sql语法大致有:

  • select database()
  • select user()
  • select last_insert_id()
  • select version()
  • select查询语句

那么在SelectHandler类里面,也实现了对这些语法的解析,在这个类里面,又需要进一步解析select后面跟着的词,来进行下一轮的switch分支处理。比如当解析到select后面跟着database(),那么就进行select database()的逻辑处理。这一步的解析任务落在ServerParseSelect类上。

感兴趣的同学自己搜索一下SelectHandler和ServerParseSelect这两个类。

2.2 深解析

前面说到,深解析是利用alibaba的druid sql parser来完成的。对于mysql语法,使用的是MysqlStatementParser来进行解析,通过以下代码,我们就可以解析得到一个sql的所有信息:


String sql = "select * from customer where id = 1";
SQLStatementParser parser = new MysqlStatementParser(sql);
SQLStatement stmt = parser.parseStatement();

druid parser的实现比较复杂(需要涉及编译原理课程词法解析器和语法解析器的原理知识),代码也多。我没有研究过它的代码,感兴趣的同学可以自己去看看。这里我们关注它的api调用既可。

SQLStatement是druid定义的一个统一的接口,不同数据库不同sql语法的statement都会实现该接口,比如上面的语句,对应的实现类是SQLSelectStatement

mycat的sql深解析逻辑代码调用耦合在sql路由模块里面,具体位置对应到DruidMycatRouteStrategyrouteNormalSqlWithAST方法里面,如下所示:


    @Override
    public RouteResultset routeNormalSqlWithAST(SchemaConfig schema,
            String stmt, RouteResultset rrs, String charset,
            LayerCachePool cachePool) throws SQLNonTransientException {

        /**
         *  只有mysql时只支持mysql语法
         */
        SQLStatementParser parser = null;
        if (schema.isNeedSupportMultiDBType()) {
            parser = new MycatStatementParser(stmt);
        } else {
            parser = new MySqlStatementParser(stmt); 
        }

        MycatSchemaStatVisitor visitor = null;
        SQLStatement statement;

        /**
         * 解析出现问题统一抛SQL语法错误
         */
        try {
            statement = parser.parseStatement();
            visitor = new MycatSchemaStatVisitor();
        } catch (Exception t) {
            LOGGER.error("DruidMycatRouteStrategyError", t);
            throw new SQLSyntaxErrorException(t);
        }

        /**
         * 检验unsupported statement
         */
        checkUnSupportedStatement(statement);


        DruidParser druidParser = DruidParserFactory.create(schema, statement, visitor);
        druidParser.parser(schema, rrs, statement, stmt,cachePool,visitor);

        // ...

    }

我们应该如何得到SQLStatement具体内容呢?在druid里面主要通过visitor方式解析和statement方式解析得到。有些类型的SQLStatement通过visitor解析足够了,但是有些只能通过statement解析才能得到所有信息,而有些需要通过两种方式解析才能得到完整信息。基于上面的原因考虑,在mycat中定义了DruidParser接口类和其对应的实现类来实现这个需求,如下类图所示:

mycat-druid-parser-classdiagram

在mycat里面通过调用DruidParser的parser方法来满足上面提到的解析需求,实现的通用逻辑代码在DefaultDruidParser的parser方法里面,如下所示:


    /**
     * 使用MycatSchemaStatVisitor解析,得到tables、tableAliasMap、conditions等
     * @param schema
     * @param stmt
     */
    public void parser(SchemaConfig schema, RouteResultset rrs, SQLStatement stmt, String originSql,LayerCachePool cachePool,MycatSchemaStatVisitor schemaStatVisitor) throws SQLNonTransientException {
        ctx = new DruidShardingParseInfo();
        //设置为原始sql,如果有需要改写sql的,可以通过修改SQLStatement中的属性,然后调用SQLStatement.toString()得到改写的sql
        ctx.setSql(originSql);
        //通过visitor解析
        visitorParse(rrs,stmt,schemaStatVisitor);
        //通过Statement解析
        statementParse(schema, rrs, stmt);

        //改写sql:如insert语句主键自增长的可以
        changeSql(schema, rrs, stmt,cachePool);
    }

visitorParsestatementParse两个方法留给子类根据实际处理情况去实现。例如,DruidSelectParser只实现了statementParse方法。

这里还有个问题,就是mycat如何确定一个具体的DruidParser实现类去进行处理? —— DruidParser由DruidParserFactorycreate方法负责创建,具体应该创建哪个DruidParser子类,是在DruidParserFactory的create方法里面根据druid解析得到的SQLStatement对象进行判断的。所以在outeNormalSqlWithAST方法解析得到SQLStatement之后,需要利用DruidParserFactory来构造具体的DruidParser。具体的判断逻辑如下代码所示:


    public static DruidParser create(SchemaConfig schema, SQLStatement statement, SchemaStatVisitor visitor)
    {
        DruidParser parser = null;
        if (statement instanceof SQLSelectStatement)
        {
            if(schema.isNeedSupportMultiDBType())
            {
                parser = getDruidParserForMultiDB(schema, statement, visitor);

            }

            if (parser == null)
            {
                parser = new DruidSelectParser();
            }
        } else if (statement instanceof MySqlInsertStatement)
        {
            parser = new DruidInsertParser();
        } else if (statement instanceof MySqlDeleteStatement)
        {
            parser = new DruidDeleteParser();
        } else if (statement instanceof MySqlCreateTableStatement)
        {
            parser = new DruidCreateTableParser();
        } else if (statement instanceof MySqlUpdateStatement)
        {
            parser = new DruidUpdateParser();
        } else if (statement instanceof SQLAlterTableStatement)
        {
            parser = new DruidAlterTableParser();
        } else if (statement instanceof MySqlLockTableStatement) {
            parser = new DruidLockTableParser();
        } else
        {
            parser = new DefaultDruidParser();
        }

        return parser;
    }

3. 总结

大部分文章会把sql解析模块和sql路由模块结合在一起讲,但在这里,我还是把它单独拆出来,单独分析它的功能和代码。虽然sql解析模块会耦合在路由模块里面,但是它并非完全为路由模块服务,它同时也为结果集后处理模块服务。我们也只有理解了sql解析模块,才能更好的理解路由模块和结果集后处理模块。

展开阅读全文

没有更多推荐了,返回首页