Sharding-JDBC源碼初探

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)圖如下:


sharding-jdbc架構(gòu)圖 by github
sharding-jdbc架構(gòu)圖 by github

基本上分庫(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類中的示例查詢。

example

規(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;
}
sharding-context

獲取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ù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容