最近在研究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等對象進行使用。當然,圍繞解析、提取、填充這三個步驟我們還有很多細節沒有展開,今天的內容就到此爲止,下一篇文章我們會更多關注這些細節。
更多內容可以關注我的公衆號:程序員向架構師轉型。