未加星标

Mycat源码学习:Mycatsql解析模块分析

字体大小 | |
[数据库(综合) 所属分类 数据库(综合) | 发布者 店小二05 | 时间 2016 | 作者 红领巾 ] 0人收藏点击收藏

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路由模块里面,具体位置对应到DruidMycatRouteStrategy的routeNormalSqlWithAST方法里面,如下所示:

@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源码学习:Mycatsql解析模块分析

在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);
}

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

这里还有个问题,就是mycat如何确定一个具体的DruidParser实现类去进行处理? —— DruidParser由DruidParserFactory的create方法负责创建,具体应该创建哪个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解析模块,才能更好的理解路由模块和结果集后处理模块。

本文数据库(综合)相关术语:系统安全软件

主题: SQL中间件开源数据法大需求数据库RY阿里
分页:12
转载请注明
本文标题:Mycat源码学习:Mycatsql解析模块分析
本站链接:http://www.codesec.net/view/480246.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 数据库(综合) | 评论(0) | 阅读(40)