Sharding-JDBC是當(dāng)當(dāng)開(kāi)源的分庫(kù)分表中間件,通過(guò)重寫jdbc api的方式為Java應(yīng)用提供分庫(kù)分表的能力,在官方項(xiàng)目首頁(yè)有很多使用說(shuō)明,相比較其他的分庫(kù)分表開(kāi)源產(chǎn)品,文檔算是維護(hù)的比較好的一個(gè)項(xiàng)目,而且開(kāi)發(fā)者也一直在維護(hù)更新,并和社區(qū)保持著積極的溝通。
sharding-jdbc的整體架構(gòu)圖如下:
基本上分庫(kù)分表中間件都要實(shí)現(xiàn)如圖中的這些功能:規(guī)則配置、SQL解析、SQL改寫、SQL路由、SQL執(zhí)行、結(jié)果集合并。下面讓我們深入到sharding-jdbc的源碼中去,粗略地看一下這些功能在代碼中是如何實(shí)現(xiàn)的。
在github上下載sharding-jdbc的源碼后,需要在IDE中添加對(duì)Lombok的支持,因?yàn)轫?xiàng)目中大量地使用了Lombok的注解,以減少項(xiàng)目代碼量。在sharding-jdbc中有個(gè)大量示例的子項(xiàng)目sharding-jdbc-example,以sharding-jdbc-example-jdbc為突破口,先執(zhí)行resources中的all_schema.sql文件建好所需要的分表,然后執(zhí)行Main類中的示例查詢。
規(guī)則配置
sharding-jdbc的規(guī)則配置稍顯復(fù)雜,規(guī)則配置的目的是為了能夠得到ShardingDataSource
對(duì)象,即包含有分片信息和物理數(shù)據(jù)源的分片數(shù)據(jù)源對(duì)象,而ShardingDataSource是繼承標(biāo)準(zhǔn)的JDBC的DataSource接口的。物理數(shù)據(jù)源是真正執(zhí)行sql的地方,每個(gè)物理數(shù)據(jù)源連接實(shí)際的database,在構(gòu)建物理數(shù)據(jù)源的過(guò)程中,可以使用數(shù)據(jù)庫(kù)連接池技術(shù)提升性能,比如常用的出c3p0、druid,而示例用的是dbcp。
private static ShardingDataSource getShardingDataSource() {
DataSourceRule dataSourceRule = new DataSourceRule(createDataSourceMap());
TableRule orderTableRule = TableRule.builder("t_order").actualTables(Arrays.asList("t_order_0", "t_order_1")).dataSourceRule(dataSourceRule).build();
TableRule orderItemTableRule = TableRule.builder("t_order_item").actualTables(Arrays.asList("t_order_item_0", "t_order_item_1")).dataSourceRule(dataSourceRule).build();
ShardingRule shardingRule = ShardingRule.builder().dataSourceRule(dataSourceRule).tableRules(Arrays.asList(orderTableRule, orderItemTableRule))
.bindingTableRules(Collections.singletonList(new BindingTableRule(Arrays.asList(orderTableRule, orderItemTableRule))))
.databaseShardingStrategy(new DatabaseShardingStrategy("user_id", new ModuloDatabaseShardingAlgorithm()))
.tableShardingStrategy(new TableShardingStrategy("order_id", new ModuloTableShardingAlgorithm())).build();
return new ShardingDataSource(shardingRule);
}
private static Map<String, DataSource> createDataSourceMap() {
Map<String, DataSource> result = new HashMap<>(2);
result.put("ds_0", createDataSource("ds_0"));
result.put("ds_1", createDataSource("ds_1"));
return result;
}
private static DataSource createDataSource(final String dataSourceName) {
BasicDataSource result = new BasicDataSource();
result.setDriverClassName(com.mysql.jdbc.Driver.class.getName());
result.setUrl(String.format("jdbc:mysql://localhost:3306/%s", dataSourceName));
result.setUsername("root");
result.setPassword("");
return result;
}
獲取數(shù)據(jù)源之后,就是執(zhí)行SQL語(yǔ)句,然后獲得結(jié)果集,這個(gè)和以往正常的通過(guò)jdbc api連接數(shù)據(jù)庫(kù)并獲取數(shù)據(jù)的流程并無(wú)實(shí)質(zhì)性差別,唯一區(qū)別的是這里的每個(gè)接口的實(shí)現(xiàn)類都是Sharding的,比如printGroupBy執(zhí)行的統(tǒng)計(jì)用戶訂單量的示例。
private static void printGroupBy(final DataSource dataSource) throws SQLException {
String sql = "SELECT o.user_id, COUNT(*) FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id GROUP BY o.user_id";
try (
Connection conn = dataSource.getConnection();
PreparedStatement preparedStatement = conn.prepareStatement(sql)
) {
ResultSet rs = preparedStatement.executeQuery();
while (rs.next()) {
System.out.println("user_id: " + rs.getInt(1) + ", count: " + rs.getInt(2));
}
}
}
關(guān)鍵的ShardingContext
在執(zhí)行dataSource.getConnection()之后獲取到了shardingConnection,shardingConnection是一種邏輯意義上的分布式數(shù)據(jù)庫(kù)的連接,其中最重要的成員變量當(dāng)屬shardingContext,即數(shù)據(jù)源運(yùn)行時(shí)的上下文信息,包括了shardingRule(分片規(guī)則)、sqlRouteEngine(數(shù)據(jù)路由引擎,得出最終可執(zhí)行的SQL語(yǔ)句)、executorEngine(執(zhí)行引擎,通過(guò)多線程的方式并行執(zhí)行多路SQL),之后的一系列的操作都離不開(kāi)這個(gè)shardingContext,對(duì)于能夠正確解析SQL并執(zhí)行正確非常關(guān)鍵,可以認(rèn)為是整個(gè)中間件執(zhí)行的核心。
/**
* 數(shù)據(jù)源運(yùn)行期上下文.
*
* @author gaohongtao
*/
@RequiredArgsConstructor
@Getter
public final class ShardingContext {
private final ShardingRule shardingRule;
private final SQLRouteEngine sqlRouteEngine;
private final ExecutorEngine executorEngine;
}
獲取shardingConnection之后,通過(guò)conn.prepareStatement(sql)獲取ShardingPreparedStatement,之后調(diào)用executeQuery()方法開(kāi)始執(zhí)行分布式的查詢操作,包括SQL解析、改寫、路由、執(zhí)行和合并。
@Override
public ResultSet executeQuery() throws SQLException {
ResultSet rs;
try {
rs = ResultSetFactory.getResultSet(
new PreparedStatementExecutor(getShardingConnection().getShardingContext().getExecutorEngine(), routeSQL()).executeQuery(), getMergeContext());
} finally {
clearRouteContext();
}
setCurrentResultSet(rs);
return rs;
}
private List<PreparedStatementExecutorWrapper> routeSQL() throws SQLException {
List<Object> parameters = getParameters();
List<PreparedStatementExecutorWrapper> result = new ArrayList<>();
//獲得sql路由后的結(jié)果
SQLRouteResult sqlRouteResult = preparedSQLRouter.route(getParameters());
MergeContext mergeContext = sqlRouteResult.getMergeContext();
setMergeContext(mergeContext);
setGeneratedKeyContext(sqlRouteResult.getGeneratedKeyContext());
//獲得所有的物理PreparedStatement的包裝器
for (SQLExecutionUnit each : sqlRouteResult.getExecutionUnits()) {
PreparedStatement preparedStatement = (PreparedStatement) getStatement(getShardingConnection().getConnection(each.getDataSource(), sqlRouteResult.getSqlStatementType()), each.getSql());
replayMethodsInvocation(preparedStatement);
getParameters().replayMethodsInvocation(preparedStatement);
result.add(new PreparedStatementExecutorWrapper(preparedStatement, parameters, each));
}
return result;
}
SQL解析、路由和改寫
這個(gè)過(guò)程是SQL處理的重要步驟,原始的邏輯上的SQL,需要轉(zhuǎn)換成實(shí)際可以執(zhí)行的SQL,比如替換成正確的庫(kù)名、表名,對(duì)于需要統(tǒng)計(jì)合并類的操作,可能一條邏輯SQL會(huì)對(duì)應(yīng)多條物理SQL。
/**
* 使用參數(shù)進(jìn)行SQL路由.
* 當(dāng)?shù)谝淮温酚蓵r(shí)進(jìn)行SQL解析,之后的路由復(fù)用第一次的解析結(jié)果.
*
* @param parameters SQL中的參數(shù)
* @return 路由結(jié)果
*/
public SQLRouteResult route(final List<Object> parameters) {
if (null == sqlParsedResult) {
sqlParsedResult = engine.parseSQL(logicSql, parameters);
tableRuleOptional = shardingRule.tryFindTableRule(sqlParsedResult.getRouteContext().getTables().iterator().next().getName());
} else {
generateId(parameters);
engine.setParameters(parameters);
for (ConditionContext each : sqlParsedResult.getConditionContexts()) {
each.setNewConditionValue(parameters);
}
}
return engine.routeSQL(sqlParsedResult);
}
SQL Parse的過(guò)程主要是解析SQL獲得抽象語(yǔ)法樹(AST),這個(gè)過(guò)程本質(zhì)上和參數(shù)是無(wú)關(guān)的,只是把SQL拆解成一個(gè)個(gè)token的過(guò)程,傳遞參數(shù)的目的主要用在改寫和路由,比如根據(jù)參數(shù)把原來(lái)的t_order路由到t_order_0和t_order_1。SQL Parse的結(jié)果理論上是可以緩存起來(lái)的,雖然這里似乎通過(guò)if (null == sqlParsedResult)
來(lái)復(fù)用了解析的結(jié)果,但其實(shí)由于該方法所在類PreparedSQLRouter每次都會(huì)重新創(chuàng)建,并沒(méi)有真正意義上的緩存利用起來(lái),只有在復(fù)用ShardingPreparedStatement的情況下才會(huì)復(fù)用sqlParsedResult(參考https://github.com/dangdangdotcom/sharding-jdbc/issues/156). 路由和改寫并不是兩個(gè)割裂的過(guò)程,而是在路由的同時(shí),就知道該向哪個(gè)分庫(kù)或分表執(zhí)行SQL,同時(shí)就改寫相應(yīng)的SQL。
執(zhí)行與合并
在SQL路由之后最終得到的結(jié)果是封裝了可執(zhí)行的物理PrepareStatement的集,并由preparedStatementExecutorWrappers來(lái)包裝,如果只有一個(gè)物理prepareStatement,直接執(zhí)行executeQuery獲取結(jié)果集即可,如果有多個(gè)物理prepareStatement,框架就會(huì)借助多線程執(zhí)行每個(gè)查詢,最后合并。
/**
* 執(zhí)行SQL查詢.
*
* @return 結(jié)果集列表
*/
public List<ResultSet> executeQuery() {
Context context = MetricsContext.start("ShardingPreparedStatement-executeQuery");
eventPostman.postExecutionEvents();
List<ResultSet> result;
final boolean isExceptionThrown = ExecutorExceptionHandler.isExceptionThrown();
final Map<String, Object> dataMap = ExecutorDataMap.getDataMap();
try {
if (1 == preparedStatementExecutorWrappers.size()) {
//直接執(zhí)行一條SQL
return Collections.singletonList(executeQueryInternal(preparedStatementExecutorWrappers.iterator().next(), isExceptionThrown, dataMap));
}
//多線程框架下執(zhí)行
result = executorEngine.execute(preparedStatementExecutorWrappers, new ExecuteUnit<PreparedStatementExecutorWrapper, ResultSet>() {
@Override
public ResultSet execute(final PreparedStatementExecutorWrapper input) throws Exception {
synchronized (input.getPreparedStatement().getConnection()) {
return executeQueryInternal(input, isExceptionThrown, dataMap);
}
}
});
} finally {
MetricsContext.stop(context);
}
return result;
}
整體上sharding-jdbc就是分這幾部分來(lái)實(shí)現(xiàn)分庫(kù)分表的功能的,框架內(nèi)部做了很多事情,應(yīng)用方要做的就是接入依賴,然后通過(guò)代碼或配置寫清楚分庫(kù)分表的規(guī)則,即可像正常模式一樣使用分布式數(shù)據(jù)庫(kù)。