Shardingsphere使用配置及內核剖析

導讀

  • 本文主要通過源碼分析Shardingsphere原理
  • 關鍵字Shardingsphere使用、Shardingsphere源碼、Shardingsphere執行流程
  • 版本:Shardingsphere 4.1.1
  • Shardingsphere 配置

    • Yaml
    • Java Config
    • SpringBoot 👍
    • Spring命名空間
  • Shardingsphere 功能

    • 數據分片 👍
    • 讀寫分離
    • 強制路由(可以歸爲數據分片得一種)
    • 數據加密
    • 分佈式事務

總之,功能很強大!!!
當然,本篇並不會分析所有功能點,而是講解最常用得數據分片配置用法以及原理

如何在 SpringBoot中配置數據分片策略 ?

項目中引入依賴:

        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.1.1</version>
        </dependency>

配置中心(Apollo、Nacos)或者本地項目中引入配置文件,添加如下配置:

spring.shardingsphere.datasource.names=ds0,ds1

spring.shardingsphere.datasource.ds0.type= com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name= com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.url= jdbc:mysql://xxxxxx:3306/ds0
spring.shardingsphere.datasource.ds0.username= xxx
spring.shardingsphere.datasource.ds0.password= xxx

spring.shardingsphere.datasource.ds1.type= com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.url=jdbc:mysql://xxxxxx:3306/ds1
spring.shardingsphere.datasource.ds1.username= xxx
spring.shardingsphere.datasource.ds1.password= xxx

spring.shardingsphere.sharding.tables.logical_table.database-strategy.inline.sharding-column = sharding_column
spring.shardingsphere.sharding.tables.logical_table.database-strategy.inline.algorithm-expression = ds$->{ShardingHash.shardingDBValue(sharding_column,2)}
spring.shardingsphere.sharding.tables.logical_table.actual-data-nodes = ds$->{0..1}.logical_table_$->{0..3}
spring.shardingsphere.sharding.tables.logical_table.table-strategy.inline.sharding-column = sharding_column
spring.shardingsphere.sharding.tables.logical_table.table-strategy.inline.algorithm-expression = logical_table_$->{ShardingHash.shardingTBValue(sharding_column,2,4)}

上述配置關鍵點:
① spring.shardingsphere.datasource.names=ds0,ds1 表示兩個數據源(一般測試分片要至少配置兩個)
② spring.shardingsphere.sharding.tables.logical_table.database-strategy.inline.sharding-column = sharding_column 表示配置得邏輯表爲 logical_table (實際上就是平常SQL文件中DML語句對應得表);數據庫得分片字段爲 sharding_column
③ spring.shardingsphere.sharding.tables.logical_table.database-strategy.inline.algorithm-expression =ds$ ->{ShardingHash.shardingDBValue(sharding_column,2)}
表示數據分片的算法是內聯,並且指定了分片表達式(通過Groovy來解析表達式,當然也有簡單的表達式 例如:sharding_column %2)。 $->{} 是標準語法,也可以是 ${} (不推薦這麼配置
④ spring.shardingsphere.sharding.tables.logical_table.actual-data-nodes = ds$->{0..1}.logical_table_$->{0..3} 表示一條sql 會被路由的真實數據節點總共可以有這麼多。
例如:此處會有兩個數據源:ds0,ds1; 4張表:logical_table_0 、logical_table_1、logical_table_2、logical_table_3, 那麼組合起來就會存在8種路由節點(意味着sql中若不帶分片鍵,會導致全表路由( 很嚴重 ),後面會分析)。
⑤ spring.shardingsphere.sharding.tables.logical_table.table-strategy.inline.sharding-column = sharding_column 表示 logical_table邏輯表對應的分表字段是 sharding_column
⑥ spring.shardingsphere.sharding.tables.logical_table.table-strategy.inline.algorithm-expression = logical_table_$->{ShardingHash.shardingTBValue(sharding_column,2,4)} 表示 分表對應的表達式(同上述分庫
⑦ 以上策略需要根據自己項目中的情況,來選取庫分片字段與表分片字段以及相應的分片算法(一般情況下,庫表分片字段最好一致,儘量的減少分佈式事務發生以及減少實際的路由節點)

到這裏,項目中已經集成了 Shardingsphere了。 用法簡單的是不是有點懷疑自己漏了一些配置(實際上配置方式就是這麼簡單~)。

Shardingsphere 如何做到分庫分表的 ?
  • 配置加載過程
    首先由於我們引入得 spring boot starter得依賴,我們大致就可以猜到:Shardingsphere 肯定也有一個 類似得自動裝配類,此處是(SpringBootConfiguration),不清楚自動裝配得可以參考一下作者之前得文章(一文讀懂SpringBoot自動裝配原理)。找到了入口,我們就直接看一下源碼:
    ,其中有四點着重分析一下。
    ① 此處表明,自動裝配在 DataSourceAutoConfiguration 這個自動裝配類之前完成。也就是Shardingsphere創建得數據源就是全局得數據源,項目只要涉及到對數據庫得任何操作都會經過ShardingDataSource得這一層處理(④中創建得)。正是基於此,爲後面得數據分片以及一些擴展埋下基礎。還有一點就是,如果我們項目中使用了Mybatis這個ORM框架的話,會發現Mybatis得starter啓動配置類是在 DataSourceAutoConfiguration 裝配之後再進行裝配得,如圖:
    那麼此時Mybatis使用得數據源就是 Shardingsphere配置得 ShardingDataSource。
    ② 將之前配置得規則映射到此配置文件中,爲創建數據源得過程提供配置信息。
    dataSourceMap 對象存放得是配置得所有數據源映射信息,爲後面獲取數據庫連接以及數據分片提供基礎能力。
    ④ 通過 ShardingDataSourceFactory 這個工廠類來創建 ShardingDataSource數據源,,創建數據源得過程中,有兩步很關鍵:
    ① 初始化路由裝飾器(路由引擎,SPI得方式,用戶可以擴展)、創建SQL改寫上下文裝飾器(改寫引擎,同上)、創建結果處理引擎(歸併引擎,用於對查詢結果合併處理,同上)
    ② 創建運行時上下文(全局分片運行時上下文,用於保存分片所需得相關配置),這兩處關鍵步驟後面會單獨分析。
    由於我們目前演示得是基於分片得策略配置,所以只有 ShardingRuleCondition 才滿足裝配條件。
    而在創建數據源得同時,會將配置得規則解析成 ShardingRule,供後續得數據庫操作提供分片核心能力。其中有一個重要得配置轉換過程。會將分表規則、分庫規則、分表算法、分庫算法等都解析到對應得 ShardingRuleConfiguration 通用分片配置類中, 如圖:,根據我們前面得配置,通過第一個for循環得解析可以將我們配置得分庫、分表策略、分庫算法、分表算法解析到TableRuleConfiguration中,每一張表都會對應一個配置類:
        for (Entry<String, YamlTableRuleConfiguration> entry : yamlConfiguration.getTables().entrySet()) {
            YamlTableRuleConfiguration tableRuleConfig = entry.getValue();
            tableRuleConfig.setLogicTable(entry.getKey());
            result.getTableRuleConfigs().add(tableRuleConfigurationYamlSwapper.swap(tableRuleConfig));
        }

SpringBootConfiguration -> ShardingDataSourceFactory -> ShardingRule -> ShardingDataSource -> ShardingRuntimeContext

  • 分片運行時上下文創建過程
    ① 如上面所說,創建數據源得時候會在構造器中將運行時上下文ShardingRuntimeContext一同創建出來,ShardingRuntimeContext得構造器如下圖:
    ,再來看一下類關係圖 ,發現運行時上下文進行了抽象,分片允許是上下文繼承了 MultipleDataSourcesRuntimeContext 多數據源運行時上下文,而多數據源運行時上下文又繼承了 AbstractRuntimeContext 抽象上下文。而創建 ShardingRuntimeContext 分片運行時上下文得時候會同時將分片規則保存在抽象類中,
    其中有幾步關鍵點:
    ① 緩存整個分片規則,爲後續得分片操作提供依據
    ② 緩存數據庫類型,用於後續執行得時候加載對應數據庫的額元數據
    ③ 創建執行引擎根據當前執行連接是否持有事務(根據我們目前得配置是沒有使用得)來決定是異步執行還是同步執行,根據配置得 executor.size 參數決定創建多少個線程得線程池。 默認不配置得話,使用 cachepool配置了就使用固定線程數得線程池
    解析引擎,用於解析SQL爲抽象語法樹,解析過程分爲詞法解析和語法解析。從3.0之後解析會全面替換爲 ANTLR

ShardingRuntimeContext-> MultipleDataSourcesRuntimeContext -> AbstractRuntimeContext-> ExecutorEngine-> SQLParserEngine

  • 分片處理過程
    當然了,前面那麼多得創建初始化過程都是爲了分片做準備,我們接着就來着重分析一下分片處理得過程,下面我們通過查詢請求來一探數據分片得整個過程:
    Ⅰ. ,當一個新鮮出爐的查詢語句執行時,首先會經過Mybatis層(前面一系列的過程此處不重點分析,有興趣的可以看一下作者之前分享的Mybatis流程圖),然後調用 queryFromDatabase方法,此方法中,通過模板抽象方法org.apache.ibatis.executor.BaseExecutor#doQuery,來找到具體的查詢實現(如果沒有特殊配置,此處是SimpleExecutor),並將查詢結果存入本地一級緩存中。而在org.apache.ibatis.executor.SimpleExecutor#doQuery中,,此處會創建一個 Preparestatement實例,而此實例就是ShardingPreparedStatement。到此,我們離Shardingsphere的內核又近了一步
    Ⅱ. 順着上述思路,我們繼續Debug往下走。接着會經過Mybatis的預編譯SQL處理器,然後調用PreparedStatement的execute方法 , 通過①中的分析的我們知道,此處的PreparedStatement是ShardingPreparedStatement,所以調用的是ShardingPreparedStatement的execute方法。
    Ⅲ. 接下來真正開始切入到Shardingsphere的執行邏輯中了。
    如圖在execute方法中,① 首先清理本地 PreparedStatementExecutor 中緩存的sql相關信息(創建執行單元的時候會將sql相關信息緩存到本地) ② 然後執行prepare方法,此方法中有兩個很關鍵的操作:執行路由策略和SQL改寫策略(這兩步是分片的核心,另外也都是可供使用者擴展的)。如圖:
    Ⅳ. 路由引擎:首先來看一下 executeRoute方法,
    ① 獲取 已經註冊的RouteDecorator類實例(前面創建數據源的時候初始化的,當然使用者也可以通過SPI的方式擴展自己所需要的)過濾掉泛型是BaseRule類型的(ShardingRule是其子類,所以重新的時候覆寫 getType方法時,一定要是BaseRule類型的)
    ② 實例化路由裝飾器
    ③ 調用模板方法 route,最終會調用到DataNodeRouter 的 executeRoute方法,如圖方法: ,此處又有兩步很關鍵的操作:① 解析引擎: 通過 SQLParserEngine 解析SQL(並且此處默認是會將解析後的語句緩存起來,也就證實了前面會什麼會先清理緩存),然後通過調用parse0方法解析SQL並緩存,如圖: ② 循環執行註冊了的路由裝飾器,目前內置的路由裝飾器有 ,我們主要分析一下 分片路由裝飾器 ShardingRouteDecorator,此處又有兩個比較關鍵的步驟:
    ① 獲取分片條件:根據不同的語句創建不同的 條件解析引擎來構造分片條件(獲取的分片條件用於在執行路由判斷時決定使用哪種分片策略)
    ② 通過工廠創建出 ShardingRouteEngine 實例,一般情況下 會創建出來 ShardingStandardRoutingEngine(沒有配置什麼騷操作的情況下),然後調用 標準路由執行引擎的 路由方法
    Ⅴ. 到這裏,終於要執行路由了!!!如圖:
    ① 根據路由節點生成路由結果 RouteResult
    ② 獲取數據節點:此處獲取的就是真實的SQL路由情況(比如:ds0.table_0),首先判斷是否使用直接路由(強制路由),若使用則走強制路由的分片算法去計算分片;然後再判斷是否根據分片條件去路由,若有的話,則根據配置的分片算法(內聯)根據分片值計算出來具體分到哪個庫哪張表;若都沒有的話,則直接走混合路由的處理邏輯。
    Ⅵ. 我們此處分析上述第二種情況,根據分片條件去執行分片。
    ① 首先獲取數據的分片路由值,再獲取表的分片路由值,然後調用route0方法根據數據庫分片路由值與表分片路由值去獲取路由
    ② 路由數據源
    ③ 路由表
    最後封裝成路由節點。
    Ⅶ. 回到 ShardingPreparedStatement中,調用initPreparedStatementExecutor()) 初始化 PreparedStatementExecutor實例 並將解析出來的執行上下文中的相關SQL語句組設置到緩存中(此處會獲取到需要執行的SQL集合,主要是通過maxConnectionsSizePerQuery每次執行時最大連接數來判斷sql執行單元應該分成幾組,maxConnectionsSizePerQuery的值默認是1。則表示,如果真實的sql由10條,那麼會拆分成10組,此時會根據 maxConnectionsSizePerQuery 是否大於10,小於的話則會選擇當前批次執行的是連接限制模式(只允許佔用一個庫的一個連接),相反則是內存限制模式,不會限制創建的連接數),然後調用執行器的執行方法:如圖:
    ① 獲取sql執行回調類(真正操作數據庫)
    ② 調用 executeCallback方法,此方法繼承自父類AbstractStatementExecutor,直接來看一下父類中的方法:,SQL執行模板 SQLExecuteTemplate類通過委派其成員 ExecutorEngine 執行引擎來執行真正的操作。
    Ⅷ. 執行引擎對拆分的SQL執行單元執行處理,如圖:
    ① 併發執行(是否是併發執行通過 是否持有事務來判斷的,例如 本地事務但是你修改爲非自動提交事務,那麼此時就是持有事務狀態,則此時就是同步執行語句)
    ② 迭代出SQL執行組的第一個,其餘的SQL異步執行
    ③ 同步執行第一個SQL執行組(方便與後面的執行組進行合併起來)
    ④ 通過其內置的線程池來異步執行SQL
    此時一條查詢語句到這裏就執行完了,接下來我們接着分析對查詢結果進行處理的操作
    Ⅸ. 再回到Mybatis中,最後對查詢的結果集進行處理( resultSetHandler.<E> handleResultSets(ps),此處是DefaultResultSetHandler 結果集處理器 ),如圖:,首先調用getFirstResultSet去獲取第一個結果集: ,此處的 Statement 實例是 ShardingPreparedStatement,所以①此處會調用其 getResultSet方法。
    Ⅹ. 結果歸併:將查詢返回的結果集進行合併處理,Shardingsphere 的歸併引擎功能上劃分:遍歷歸併、排序歸併(SQL中存在ORDER BY語句)、分組歸併(SQL中有GroupBy子句)、聚合歸併(含有聚合函數)、分頁歸併(含有Limit關鍵字),歸併引擎的詳細介紹請參閱:歸併引擎,如圖:
    ① 獲取所有Statement對應的結果集,此處是拿到真正數據源所對應的Statement實例,比如:我現在的數據源是 HikariDateSource,那麼拿到的就是 HikariProxyPreparedStatement.如圖:
    執行合併邏輯:首先將結果集封裝成流式查詢結果對象StreamQueryResult,接着創建合併引擎 MergeEngine,然後調用合併引擎的合併方法:
    ③ 實例化合並引擎處理器ResultProcessEngine
    ④ 調用MergeEntry的 process 方法,委派來進行合併邏輯。
    ⑤ ⑥ 中,判斷若是 ResultMergerEngine類型的合併引擎,則調用其merge方法執行真正的合併邏輯。如下圖 , 顯然滿足類型判斷,則此處會調用ShardingResultMergerEngine#newInstance 方法來實例化真正用於合併數據流的引擎。 ,顯然此處是查詢語句,那麼最終用於合併的引擎就是 ShardingDQLResultMerger,然後執行其merge方法。如圖:
    ⑦ 中判斷sql中包含哪些關鍵字,然後創建對應的合併結果,如果條件都不滿足,那麼默認會使用 遍歷流式歸併方式合併數據。假設 我們此處SQL中帶有 order by關鍵字,那麼創建得合併結果對象就是OrderByStreamMergedResult
    ⑧ 對創建出來的排序合併結果進行裝飾操作(就是判斷有沒有別的關鍵字,例如:Limit,如果有就會創建LimitDecoratorMergedResult 裝飾器對象,在之前的排序合併基礎上又多一個 Limit功能),再回到 ShardingPreparedStatement中,會創建一個 ShardingResultSet對象設置到當前的成員變量currentResultSet中,並返回。 此時如果是批量的場景,返回的結果集中實際上已經包含了所有的結果集(前面存放在OrderByStreamMergedResult的 orderByValuesQueue 隊列中),引用官方的一個圖就是:,當調用合併結果的 next方法時會執行如圖的流程:,最後流程又回到Mybatis 結果集處理上了,將結果返回給請求調用方。

寫在最後:

  • 如果你從來沒接觸過Shardingsphere,建議先去了解一下,然後再結合作者本篇源碼剖析,可能會對你更有幫助 Shardingsphere 中文官網
  • 忙了很久一段時間(比996還嚴重得那種😫),好久沒有更新了,發現文采都沒有以前那麼好了🤭,大夥湊合看吧
  1. ☛ 文章要是勘誤或者知識點說的不正確,歡迎評論,畢竟這也是作者通過閱讀源碼獲得的知識,難免會有疏忽!
  2. 要是感覺文章對你有所幫助,不妨點個關注,或者移駕看一下作者的其他文集,也都是幹活多多哦,文章也在全力更新中。
  3. 著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處! ·
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章