Sharding-JDBC源碼分析

系統層級

Sharding-JDBC本質是JDBC的增強持寄,使服務能夠實現數據的分布式存儲效果稍味∧B可查看如何理解ShardingSphere油宜?。深入ShardingSphere之前需要了解其定義的相關基本概念如分片社牲、邏輯表歇盼、物理表、廣播表搞莺、分片算法分類等掂咒,具體可以查看官網绍刮。如下圖所示Sharding-JDBC整體還是屬于數據訪問層的孩革,在數據訪問層中處于ORM框架之下和ORM是完全解耦的膝蜈,所以他是可以完全兼容各種類型的ORM框架饱搏。Sharding-JDBC對jdbc-connector進行了封裝,對其核心的四大對象重新進行了實現备绽,在實現中加入了相關的內核邏輯疯坤,包括:SQL解析压怠、SQL路由菌瘫、 SQL改寫、SQL執(zhí)行雇盖、SQL歸并等核心邏輯崔挖。本文所有分析和文檔基于版本4.1.1

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.1.1</version>
</dependency>
Sharding-JDBC層級圖.png

調用過程

ShardingStatement執(zhí)行一次查詢的過程為例分析具體的調用過程。

sharding-jdbc調用過程.png

執(zhí)行的入口都是在ShardingStatement中脓鹃,StatementExecutor封裝了SQL解析、重寫的核心過程古沥。MergeEngine負責統籌結果的合并瘸右,最后返回合并結果在ShardingStatement中封裝成ShardingResultSet返回。

JDBC增強

  • java.sql.Wrapper提供了判斷當前類是否是目標包裝類和反包裝為目標類對象的API岩齿,目的是為了方便調用太颤,將工具類提供的功能放到對象維度去使用,跟設計模式中的包裝器模式沒什么關系纯衍。
  • org.apache.shardingsphere.shardingjdbc.jdbc.adapter.WrapperAdapter實現了java.sql.Wrapper的兩個工具方法栋齿,同時增加了兩個記錄調用方法和回放調用方法的API和容器來存儲回放方法列表襟诸。該類重點是增加支持記錄方法和調用方法瓦堵,而非適配。

為什么需要調用方法的記錄和回放歌亲?

針對JDBC四大對象Sharding-JDBC是重新做了封裝菇用,而對實際的四大對象的一些方法調用往往發(fā)生在SQL路由操作完成之后,所以需要提前記錄之后回放陷揪。

  • 所有AbstractUnsupportedXXX對象代表了對不支持的操作的默認實現(拋異常SQLFeatureNotSupportedException)惋鸥。

配置

sharding-JDBC的自動化配置類是:
org.apache.shardingsphere.shardingjdbc.spring.boot.SpringBootConfiguration主要是針對配置參數和不同的場景的數據源進行了配置。DataSource在應用的實例只能存在一份悍缠,不同的場景通過注解@Conditional的配置判斷不同的配置參數卦绣。
啟動類注解@AutoConfigureBefore(DataSourceAutoConfiguration.class)表明它啟動在Spring管理的數據源自動化配置DataSourceAutoConfiguration之前能很好兼容歷史數據源以及配置。

DataSource

DataSource.png

抽象數據源適配器對象: org.apache.shardingsphere.shardingjdbc.jdbc.adapter.AbstractDataSourceAdapter做了兩點適配:

  • AutoCloseable的實現飞蚓,在close方法中關閉資源滤港。
  • 增加了org.apache.shardingsphere.shardingjdbc.jdbc.core.context.RuntimeContext實現類Getter方法,用于支撐相關的JDBC-Sharding操作趴拧。

針對不同場景的數據源都作出了對應的實現溅漾,不同的實現關鍵區(qū)別點主要是在RuntimeContext的實現不同山叮、不同Connection對象的實現和每個實現靜態(tài)代碼中初始化注冊的裝飾對象的不同。例如:

public class ShardingDataSource extends AbstractDataSourceAdapter {
    
    private final ShardingRuntimeContext runtimeContext;
    
    static {
        NewInstanceServiceLoader.register(RouteDecorator.class);
        NewInstanceServiceLoader.register(SQLRewriteContextDecorator.class);
        NewInstanceServiceLoader.register(ResultProcessEngine.class);
    }
    //...ignore...
}


public class MasterSlaveDataSource extends AbstractDataSourceAdapter {
    
    private final MasterSlaveRuntimeContext runtimeContext;
    
    static {
        NewInstanceServiceLoader.register(RouteDecorator.class);
    }
    //...ignore...
}
...

這種在靜態(tài)代碼塊中的通過注冊不同的Java SPI實現添履,可以完成對不同的場景的特殊處理屁倔。可以把這種處理邏輯理解成 SPI裝飾層:針對不同業(yè)務場景(主從暮胧、加密锐借、正常分片)的核心邏輯(SQL路由、SQL重新叔壤,結果集歸并)進行的獨立裝飾處理的一層瞎饲,實現方式是Java SPI。

Connection

Connetion.png

org.apache.shardingsphere.shardingjdbc.jdbc.adapter.AbstractConnectionAdapter:增加了獲取實際數據庫連接對象的方法炼绘,同時將連接對象的創(chuàng)建留給子類實現。獲取連接列表時針對不同鏈接模式MEMORY_STRICTLY, CONNECTION_STRICTLY會有不同操作妄田。內存限制模式時鏈接沒有限制可以并發(fā)的請求數據然后在內存中做歸并俺亮,如果一個鏈接一個鏈接的獲取可能存在饑餓等待導致死鎖所以需要加鎖并一次性獲取所有連接適合OLAP業(yè)務。連接限制模式將結果集裝載在內存之后直接釋放資源不需要加鎖疟呐,保證了資源的使用率適合OLTP業(yè)務脚曾。
關于鏈接模式的判斷邏輯為每個連接執(zhí)行SQL的數量,執(zhí)行1個為內存限制模式启具,執(zhí)行1個以上為連接限制模式本讥,代碼邏輯如下:
org.apache.shardingsphere.sharding.execute.sql.prepare.SQLExecutePrepareTemplate#getSQLExecuteGroups

private List<InputGroup<StatementExecuteUnit>> getSQLExecuteGroups(final String dataSourceName,
                                                                       final List<SQLUnit> sqlUnits, final SQLExecutePrepareCallback callback) throws SQLException {
        List<InputGroup<StatementExecuteUnit>> result = new LinkedList<>();
        int desiredPartitionSize = Math.max(0 == sqlUnits.size() % maxConnectionsSizePerQuery ? sqlUnits.size() / maxConnectionsSizePerQuery : sqlUnits.size() / maxConnectionsSizePerQuery + 1, 1);
        List<List<SQLUnit>> sqlUnitPartitions = Lists.partition(sqlUnits, desiredPartitionSize);
        ConnectionMode connectionMode = maxConnectionsSizePerQuery < sqlUnits.size() ? ConnectionMode.CONNECTION_STRICTLY : ConnectionMode.MEMORY_STRICTLY;
        List<Connection> connections = callback.getConnections(connectionMode, dataSourceName, sqlUnitPartitions.size());
        int count = 0;
        for (List<SQLUnit> each : sqlUnitPartitions) {
            result.add(getSQLExecuteGroup(connectionMode, connections.get(count++), dataSourceName, each, callback));
        }
        return result;
    }

org.apache.shardingsphere.underlying.common.hook.SPIRootInvokeHook:通過SPI實現在鏈接創(chuàng)建和關閉處埋點用于可能的邏輯擴展。

Statement

Statement.png

org.apache.shardingsphere.shardingjdbc.jdbc.adapter.AbstractStatementAdapter增加了如下兩個適配方法鲁冯,其直接子類均是普通Statement對象拷沸。

protected abstract boolean isAccumulate();//用于判斷是否返回累加結果作為更新影響數。
protected abstract Collection<? extends Statement> getRoutedStatements();//獲取路由后的語句對象列表薯演。

預定制的Statement均繼承了org.apache.shardingsphere.shardingjdbc.jdbc.adapter.AbstractShardingPreparedStatementAdapter:實現了PreparedStatement相關的參數設置外撞芍,單獨維護了參數設置方法調用的列表并提供了setter和回放調用,主要用于實際SQL執(zhí)行之前跨扮、邏輯SQL路由之后SQL參數的設置序无。sharding-jdbc中相關Statement實現類是相關內核邏輯執(zhí)行的入口。

ResultSet

ResultSet.png

org.apache.shardingsphere.shardingjdbc.jdbc.adapter.AbstractResultSetAdapter:僅僅做了ResultSet的一些抽象實現未做其他適配衡创。ResultSet的相關實現類針對不同場景結果集做了不同的封裝和實現帝嗡。

內核邏輯

SQL解析

SQL解析是Sharding-JDBC進行路由的開始,主要分為幾個步驟:

  1. 通過SQL解析器將SQL解析成AST(抽象語法樹)
  2. 通過抽取器將語法樹抽取成SQL片段
  3. 通過填充器將片段拼接成解析結果
  4. 通過優(yōu)化器輸出最后的結果
    相關概念可參考官網

代碼層面解析邏輯由org.apache.shardingsphere.sql.parser.SQLParserEngine#parse作為入口璃氢,本方法中加入的SQL解析的SPI埋點:org.apache.shardingsphere.sql.parser.hook.SPIParsingHook
org.apache.shardingsphere.sql.parser.SQLParserEngine#parse0調用對象進行解析并加入了緩存的邏輯哟玷。

public final class SQLParserEngine {
    private final String databaseTypeName;
    private final SQLParseResultCache cache = new SQLParseResultCache();
    /** * Parse SQL. * * @param sql SQL * @param useCache use cache or not * @return SQL statement */
    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;
            // CHECKSTYLE:OFF
        } catch (final Exception ex) {
            // CHECKSTYLE:ON
            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();
            }
        }
        ParseTree parseTree = new SQLParserExecutor(databaseTypeName, sql).execute().getRootNode();
        SQLStatement result = (SQLStatement) ParseTreeVisitorFactory.newInstance(databaseTypeName, VisitorRule.valueOf(parseTree.getClass())).visit(parseTree);
        if (useCache) {
            cache.put(sql, result);
        }
        return result;
    }
}

SQL路由

根據不同的場景對SQL的路由也分為不同的方式,參考下圖:
SQL路由
具體參考官網文檔

SQL的解析和路由發(fā)生在Statement對應SQL執(zhí)行方法exeXXXX的準備階段拔莱。

  • 準備階段的邏輯繼續(xù)分層和下沉首先會到準備引擎BasePrepareEngine(SimpleQueryPrepareEngine/PreparedQueryPrepareEngine)碗降,兩個實現類的區(qū)別在于是否使用SQL解析的緩存(PreparedQueryPrepareEngine用緩存隘竭,推薦)。同時在這層加載SPI裝飾層對象讼渊。
  • 接著進入下層org.apache.shardingsphere.underlying.route.DataNodeRouter动看,這層主要是增加了SPI埋點org.apache.shardingsphere.underlying.route.hook.SPIRoutingHook:分為開始,成功爪幻,失敗三階段菱皆。接著傳遞到下層。
  • 本層解析引擎org.apache.shardingsphere.sql.parser.SQLParserEngine負責解析挨稿,并將解析的SQLStatement包裝在上下文SQLStatementContext中作為整體構成RouteContext返回仇轻。
  • 此時上下文中已經持有SQL的解析結果的上下文傳遞到SPI路由裝飾層進行實際的路由。本層中會更加Statement的不同獲取不同策略的路由引擎(org.apache.shardingsphere.sharding.route.engine.type.ShardingRouteEngine)實現類進行路由奶甘,然后得到路由結果篷店,至此準備階段結束。

SQL改寫

SQL改寫主要做什么臭家?直接查看官方說明疲陕。改表名索引之類的標識符、補列钉赁、分頁修正蹄殃、優(yōu)化的范疇。
同樣是在準備階段你踩,SQL路由完成返回了路由上下文(RouteContext)之后org.apache.shardingsphere.underlying.pluggble.prepare.BasePrepareEngine#executeRewrite方法中:

private Collection<ExecutionUnit> executeRewrite(final String sql, final List<Object> parameters, final RouteContext routeContext) {
    registerRewriteDecorator();
    SQLRewriteContext sqlRewriteContext = rewriter.createSQLRewriteContext(sql, parameters, routeContext.getSqlStatementContext(), routeContext);
    return routeContext.getRouteResult().getRouteUnits().isEmpty() ? rewrite(sqlRewriteContext) : rewrite(routeContext, sqlRewriteContext);
}
  • SPI裝飾層對象加載org.apache.shardingsphere.underlying.rewrite.context.SQLRewriteContextDecorator
  • 進入org.apache.shardingsphere.underlying.rewrite.SQLRewriteEntry#createSQLRewriteContext完成上下文的創(chuàng)建并執(zhí)行裝飾層邏輯诅岩。
  • 接著調用org.apache.shardingsphere.underlying.rewrite.engine.SQLRouteRewriteEngine重寫引擎執(zhí)行重寫邏輯。

SQL執(zhí)行

SQL在解析带膜、路由吩谦,初始化后進入了執(zhí)行環(huán)節(jié)。

  • 首先由執(zhí)行器org.apache.shardingsphere.shardingjdbc.executor.AbstractStatementExecutor#exeXXX進入執(zhí)行钱慢,維護關鍵的執(zhí)行邏輯并以匿名內部類的方式將邏輯下傳逮京,如下例:org.apache.shardingsphere.shardingjdbc.executor.StatementExecutor#executeQuery
public List<QueryResult> executeQuery() throws SQLException {
    final boolean isExceptionThrown = ExecutorExceptionHandler.isExceptionThrown();
    SQLExecuteCallback<QueryResult> executeCallback = new SQLExecuteCallback<QueryResult>(getDatabaseType(), isExceptionThrown) {
        
        @Override
        protected QueryResult executeSQL(final String sql, final Statement statement, final ConnectionMode connectionMode) throws SQLException {
            return getQueryResult(sql, statement, connectionMode);
        }
    };
    return executeCallback(executeCallback);
}

private QueryResult getQueryResult(final String sql, final Statement statement, final ConnectionMode connectionMode) throws SQLException {
    ResultSet resultSet = statement.executeQuery(sql);
    getResultSets().add(resultSet);
    return ConnectionMode.MEMORY_STRICTLY == connectionMode ? new StreamQueryResult(resultSet) : new MemoryQueryResult(resultSet);
}
  • 然后進入org.apache.shardingsphere.sharding.execute.sql.execute.SQLExecuteTemplate執(zhí)行模板,由模板調度執(zhí)行引擎:org.apache.shardingsphere.underlying.executor.engine.ExecutorEngine
  • 執(zhí)行引擎org.apache.shardingsphere.underlying.executor.engine.ExecutorEngine負責發(fā)起執(zhí)行束莫,然后同步或者異步執(zhí)行懒棉。
  • 執(zhí)行的邏輯單元封裝咋對象org.apache.shardingsphere.sharding.execute.sql.execute.SQLExecuteCallback中,如下的execute0方法览绿,封裝了異常處理策严、SQL執(zhí)行SPI埋點``、SQL執(zhí)行饿敲。
private T execute0(final StatementExecuteUnit statementExecuteUnit, final boolean isTrunkThread, final Map<String, Object> dataMap) throws SQLException {
    ExecutorExceptionHandler.setExceptionThrown(isExceptionThrown);
    DataSourceMetaData dataSourceMetaData = getDataSourceMetaData(statementExecuteUnit.getStatement().getConnection().getMetaData());
    SQLExecutionHook sqlExecutionHook = new SPISQLExecutionHook();
    try {
        ExecutionUnit executionUnit = statementExecuteUnit.getExecutionUnit();
        sqlExecutionHook.start(executionUnit.getDataSourceName(), executionUnit.getSqlUnit().getSql(), executionUnit.getSqlUnit().getParameters(), dataSourceMetaData, isTrunkThread, dataMap);
        T result = executeSQL(executionUnit.getSqlUnit().getSql(), statementExecuteUnit.getStatement(), statementExecuteUnit.getConnectionMode());
        sqlExecutionHook.finishSuccess();
        return result;
    } catch (final SQLException ex) {
        sqlExecutionHook.finishFailure(ex);
        ExecutorExceptionHandler.handleException(ex);
        return null;
    }
}

SQL歸并

SQL的歸并主要針對于多節(jié)點返回到的數據進行處理的過程妻导,相關的介紹參考官網

  • SQL的歸并依賴于當前的連接模式ConnectionMode,參考上文Connection部分,連接模式決定了SQL執(zhí)行的返回結果org.apache.shardingsphere.sharding.execute.sql.execute.result.StreamQueryResult or org.apache.shardingsphere.sharding.execute.sql.execute.result.MemoryQueryResult
  • 進入歸并引擎org.apache.shardingsphere.underlying.pluggble.merge.MergeEngine#merge進行歸并操作倔韭。MergeEngine會加載SPI裝飾層的處理引擎 org.apache.shardingsphere.underlying.merge.engine.ResultProcessEngine并注冊到org.apache.shardingsphere.underlying.merge.MergeEntry中术浪。
  • 然后進入org.apache.shardingsphere.underlying.merge.MergeEntry#process進行處理。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末寿酌,一起剝皮案震驚了整個濱河市胰苏,隨后出現的幾起案子,更是在濱河造成了極大的恐慌醇疼,老刑警劉巖硕并,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異秧荆,居然都是意外死亡倔毙,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門乙濒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來陕赃,“玉大人,你說我怎么就攤上這事颁股】” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵豌蟋,是天一觀的道長。 經常有香客問我桑滩,道長梧疲,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任运准,我火速辦了婚禮幌氮,結果婚禮上,老公的妹妹穿的比我還像新娘胁澳。我一直安慰自己该互,他們只是感情好,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布韭畸。 她就那樣靜靜地躺著宇智,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胰丁。 梳的紋絲不亂的頭發(fā)上随橘,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機與錄音锦庸,去河邊找鬼机蔗。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的萝嘁。 我是一名探鬼主播梆掸,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼牙言!你這毒婦竟也來了酸钦?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嬉挡,失蹤者是張志新(化名)和其女友劉穎钝鸽,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體庞钢,經...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡拔恰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了基括。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颜懊。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖风皿,靈堂內的尸體忽然破棺而出河爹,到底是詐尸還是另有隱情,我是刑警寧澤桐款,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布咸这,位于F島的核電站,受9級特大地震影響魔眨,放射性物質發(fā)生泄漏媳维。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一遏暴、第九天 我趴在偏房一處隱蔽的房頂上張望侄刽。 院中可真熱鬧,春花似錦朋凉、人聲如沸州丹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽墓毒。三九已至,卻和暖如春盖灸,著一層夾襖步出監(jiān)牢的瞬間蚁鳖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工赁炎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留醉箕,地道東北人钾腺。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像讥裤,于是被迫代替她去往敵國和親放棒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內容