最近在研究ShardingSphere(https://shardingsphere.apache.org/index_zh.html)的源代码,准备开始写一个系列来讲解这个分库分表中间件的设计思想和实现原理。我们首先关注的是SQL解析引擎部分的内容,这部分内容也非常多,我们应该会花几篇文章对其进行讲解,这是这部分内容系列的第一篇。
想要阅读源码,首先得找到系统的入口,我们可以从ShardingSphere官网的这段使用示例代码来明确这一点:
// 配置真实数据源
Map<String, DataSource> dataSourceMap = new HashMap<>();
// 配置第一个数据源
BasicDataSource dataSource1 = new BasicDataSource();
dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
dataSource1.setUrl("jdbc:mysql://localhost:3306/ds0");
dataSource1.setUsername("root");
dataSource1.setPassword("");
dataSourceMap.put("ds0", dataSource1);
// 配置第二个数据源
BasicDataSource dataSource2 = new BasicDataSource();
dataSource2.setDriverClassName("com.mysql.jdbc.Driver");
dataSource2.setUrl("jdbc:mysql://localhost:3306/ds1");
dataSource2.setUsername("root");
dataSource2.setPassword("");
dataSourceMap.put("ds1", dataSource2);
// 配置Order表规则
TableRuleConfiguration orderTableRuleConfig = new TableRuleConfiguration("t_order","ds${0..1}.t_order${0..1}");
// 配置分库 + 分表策略
orderTableRuleConfig.setDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration("user_id", "ds${user_id % 2}"));
orderTableRuleConfig.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration("order_id", "t_order${order_id % 2}"));
// 配置分片规则
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
shardingRuleConfig.getTableRuleConfigs().add(orderTableRuleConfig);
// 省略配置order_item表规则...
// ...
// 获取数据源对象
DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties());
这里我们构建了几个数据源,加上分库、分表策略以及分片规则,然后通过ShardingDataSourceFactory获取了数据源DataSource ,显然DataSource就是我们的入口。围绕DataSource,通过跟踪代码的调用链路,我们可以得到如下所示的类层结构图:
关于这段代码示例以及这张图,我们在后面的文章中还会经常见到,今天我们关注的是整个链路中的最底层对象,即图中的SQLParseEngine。一方面,DataSource的创建过程中最终创建了SQLParseEngine,另一方面,负责执行路由功能的ShardingRouter也依赖于SQLParseEngine。这个SQLParseEngine就是整个ShardingSphere中负责SQL解析的核心。
AbstractRuntimeContext是SQLParseEngine的构建入口,构建SQLParseEngine的代码如下所示:
protected AbstractRuntimeContext(final T rule, final Properties props, final DatabaseType databaseType) {
this.rule = rule;
this.props = new ShardingProperties(null == props ? new Properties() : props);
this.databaseType = databaseType;
executeEngine = new ShardingExecuteEngine(this.props.<Integer>getValue(ShardingPropertiesConstant.EXECUTOR_SIZE));
parseEngine = SQLParseEngineFactory.getSQLParseEngine(DatabaseTypes.getTrunkDatabaseTypeName(databaseType));
ConfigurationLogger.log(rule.getRuleConfiguration());
ConfigurationLogger.log(props);
}
围绕SQLParseEngine,我们先来看它的工厂类SQLParseEngineFactory,代码如下:
public final class SQLParseEngineFactory {
private static final Map<String, SQLParseEngine> ENGINES = new ConcurrentHashMap<>();
public static SQLParseEngine getSQLParseEngine(final String databaseTypeName) {
if (ENGINES.containsKey(databaseTypeName)) {
return ENGINES.get(databaseTypeName);
}
synchronized (ENGINES) {
if (ENGINES.containsKey(databaseTypeName)) {
return ENGINES.get(databaseTypeName);
}
SQLParseEngine result = new SQLParseEngine(databaseTypeName);
ENGINES.put(databaseTypeName, result);
return result;
}
}
}
可以看到这里做了一层缓存处理,再来看SQLParseEngine本身:
public final class SQLParseEngine {
private final String databaseTypeName;
private final SQLParseResultCache cache = new SQLParseResultCache();
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;
} catch (final Exception ex) {
parsingHook.finishFailure(ex);
throw ex;
}
}
private SQLStatement parse0(final String sql, final boolean useCache) {
if (useCache) {
Optional<SQLStatement> cachedSQLStatement = cache.getSQLStatement(sql);
if (cachedSQLStatement.isPresent()) {
return cachedSQLStatement.get();
}
}
SQLStatement result = new SQLParseKernel(ParseRuleRegistry.getInstance(), databaseTypeName, sql).parse();
if (useCache) {
cache.put(sql, result);
}
return result;
}
}
关于SQLParseEngine有几点值得注意。首先,这里使用了ParsingHook作为系统运行时的钩子管理,ParsingHook定义如下:
public interface ParsingHook {
void start(String sql);
void finishSuccess(SQLStatement sqlStatement);
void finishFailure(Exception cause);
}
ShardingSphere实现了一系列的ParsingHook,类层关系如下所示,后续我们在讨论到ShardingSphere的链路跟踪时会对OpenTracingParsingHook有进一步展开。
其次,这里同样基于google的Cache类构建了SQLParseResultCache,以对解析出来的SQLStatement进行缓存处理。
然后,我们发现SQLParseEngine把真正的解析工作转移给了SQLParseKernel,在该类中,发现了如下所示的三个核心对象定义:
private final SQLParserEngine parserEngine;
private final SQLSegmentsExtractorEngine extractorEngine;
private final SQLStatementFillerEngine fillerEngine;
这三个Engine类独立的完成了三件事情,即SQL的解析(SQLParser)、SQLSegment的提取(SQLSegmentExtractor)以及SQLStatement的填充(SQLStatementFiller)。基于ShardingSphere官网的描述,这是ShardingSphere新一代SQL解析引擎的核心组成部分,其整体的架构下图所示(来自ShardingSphere官网)。
我们参考SQLParseKernel的实现来更好的理解上图中的结构,SQLParseKernel的parse方法如下所示:
public SQLStatement parse() {
//利用ANTLR4 解析SQLAST,即SQL的抽象语法树
SQLAST ast = parserEngine.parse();
//提取AST中的Token,封装成对应的Segment,如TableSegment、IndexSegment
Collection<SQLSegment> sqlSegments = extractorEngine.extract(ast);
Map<ParserRuleContext, Integer> parameterMarkerIndexes = ast.getParameterMarkerIndexes();
//填充SQLStatement并返回
return fillerEngine.fill(sqlSegments, parameterMarkerIndexes.size(), ast.getSqlStatementRule());
}
这样我们看到解析、提取、填充这三个步骤构成的整体流程已经完成,现在能够根据一条SQL语句解析出对应的SQLStatement对象供后续的ShardingRoute等对象进行使用。当然,围绕解析、提取、填充这三个步骤我们还有很多细节没有展开,今天的内容就到此为止,下一篇文章我们会更多关注这些细节。
更多内容可以关注我的公众号:程序员向架构师转型。