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里面通过调用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解析模块,才能更好的理解路由模块和结果集后处理模块。