Sharding-JDBC 系列
- 第一篇 Sharding-JDBC 源码之启动流程分析
- 第二篇 Sharding-JDBC 源码之 SQL 解析(本文)
- 第三篇 Sharding-JDBC 源码之 SQL 路由
- 第四篇 Sharding-JDBC 源码之 SQL 改写
- 第五篇 Sharding-JDBC 源码之 SQL 执行
- 第六篇 Sharding-JDBC 源码之结果集归并
在执行 SQL 时,Sharding-JDBC 需要根据启动时获取到的数据源、路由等配置文件获取真正执行的 SQL,那在获取真正执行的语句之前还需要对 SQL 进行解析、路由,今天我们来看下 Sharding-JDBC 是如何解析 SQL 语句的。
本文以查询语句为例进行分析,数据源及分库分表配置和之前保持一致。
从源码看,SQL 解析工作主要在 Sharding-JDBC 源码之 SQL 解析 的 parsing 包中:
解析过程分为词法解析和语法解析。 词法解析器用于将SQL拆解为不可再分的原子符号,称为Token。并根据不同数据库方言所提供的字典,将其归类为关键字,表达式,字面量和操作符。 再使用语法解析器将SQL转换为抽象语法树。
- cache 包:缓存解析过 SQL 语句;
- lexer 包:词法解析,封装不同方言(Mysql、Oracle 等)的解析器,使用
LexerEngine
完成词法分析; - parser 包:语法解析,根据 SQL 语句的类型完成解析工作。
在看具体执行流程前,先了解下 parsing 封装的集中 SQL 语句类型:
- dal:数据访问语言(Data Access Language) 例: use、desc、describe、show
- ddl:数据定义语言(Data Definition Language) 例:create、alter、drop、truncate
- dml:数据操作语言(Data Manipulation Language) 例: insert、update、delete
- dql:数据查询语言(Data Query Language) 例: select
- tcl:事务控制语言(Transaction Control Language) 例: set、commit、rollback、savepoint、begin。
在执行语句时,需要进行路由,而进行路由,需要对 SQL 进行解析,下面看下解析的具体执行流程:
获取 SQL 解析引擎
由于我们使用的是标准分片策略,所以获取到的解析引擎是 SQLParsingEngine
,而该引擎中的关键方法是 parse
:
public final class ParsingResultCache {
// 省略部分代码
// 逻辑 SQL 缓存
private volatile Map<String, SQLStatement> cache = new WeakHashMap<>(65535, 1);
// 省略部分代码
}
public final class SQLParsingEngine {
......
public SQLStatement parse(final boolean useCache) {
// 从缓存中获取解析结果
Optional<SQLStatement> cachedSQLStatement = getSQLStatementFromCache(useCache);
// 缓存中存在则直接获取返回解析结果
if (cachedSQLStatement.isPresent()) {
return cachedSQLStatement.get();
}
// 根据数据库类型获取具体方言词法分析器
LexerEngine lexerEngine = LexerEngineFactory.newInstance(dbType, sql);
lexerEngine.nextToken();
// 根据逻辑 SQL 类型获取具体语句解析器,并解析语言
SQLStatement result = SQLParserFactory.newInstance(dbType, lexerEngine.getCurrentToken().getType(), shardingRule, lexerEngine, shardingMetaData).parse();
// 将解析结果缓存到 cache 对象中
if (useCache) {
ParsingResultCache.getInstance().put(sql, result);
}
return result;
}
// 从缓存中获取解析结果
private Optional<SQLStatement> getSQLStatementFromCache(final boolean useCache) {
return useCache ? Optional.fromNullable(ParsingResultCache.getInstance().getSQLStatement(sql)) : Optional.<SQLStatement>absent();
}
}
ParsingResultCache
类中的cache
对象主要用来缓存解析过的逻辑 SQL,避免重复解析。同时cache
使用的是WeakHashMap
,这样可以保证不用手动释放引用且保证内存不被过度使用,因为弱引用对象只能存活到下一次 GC 之前,
缓存的 key 形如:select goods_name from t_goods where goods_id=?
。- 本例中,是 select 查询语句,所以获取的是
MySQLSelectParser
,执行该解析器的parseInternal
方法,其他类型的语句根据解析器的不同,在执行parse
方法时进入具体的实现类中。
解析 SQL
解析逻辑 SQL,将解析结果封装为 SelectStatement
对象
public MySQLSelectParser(final ShardingRule shardingRule, final LexerEngine lexerEngine, final ShardingMetaData shardingMetaData) {
// 1. 创建 DQL 门面解析器
super(shardingRule, lexerEngine, new MySQLSelectClauseParserFacade(shardingRule, lexerEngine), shardingMetaData);
selectOptionClauseParser = new MySQLSelectOptionClauseParser(lexerEngine);
limitClauseParser = new MySQLLimitClauseParser(lexerEngine);
}
/**
* 2. 解析具体逻辑 SQL
*/
protected void parseInternal(final SelectStatement selectStatement) {
parseDistinct();
parseSelectOption();
// 解析待查询的列,获取数据库字段名和字段别名
parseSelectList(selectStatement, getItems());
// 解析获取逻辑表
parseFrom(selectStatement);
// 获取查询条件
parseWhere(getShardingRule(), selectStatement, getItems());
parseGroupBy(selectStatement);
parseHaving();
parseOrderBy(selectStatement);
parseLimit(selectStatement);
parseSelectRest();
}
- 创建
MySQLSelectClauseParserFacade
对象,初始化 DQL 语句门面解析器,为下面parseInternal
做准备; - 解析具体逻辑 SQL,将语句按照 token 类型封装为
SelectStatement
对象,为后面 SQL 改写做准备。
总结
- 具体的词法分析并没有展开,主要是根据数据库类型获取对应的词法解析器,本文以 Mysql 为例,因此获取的是
MySQLLexer
,接下来词法分析引擎LexerEngine
获取到第一个有效的token
,并根据此 token 获取 SQL 解析器,本例中逻辑 SQL 第一个有效的 token 是select
,所以获得的解析器是MySQLSelectParser
; - 整体上,语句解析过程还是很简单的,就是将逻辑 SQL 按照 token 类型解析为
SQLStatement
,为后面 SQL 路由改写做准备。不过,词法分析还是略复杂,有兴趣可自行 debug 跟踪学习。