Apache ShardingSphere 後設資料載入剖析

SphereEx發表於2021-08-02

唐國強

小米軟體工程師,主要負責 MIUI 瀏覽器服務端研發工作。熱愛開源,熱愛技術,喜歡探索,熱衷於研究學習各種開源中介軟體,很高興能參與到 ShardingSphere 社群建設中,希望在社群中努力提高自己,為 ShardingSphere 社群的發展做更多的工作。


後設資料在 ShardingSphere 中載入的過程


一、概述

後設資料是表示資料的資料。從資料庫角度而言,則概括為資料庫的任何資料都是後設資料,因此如列名、資料庫名、使用者名稱、表名等以及資料自定義庫表儲存的關於資料庫物件的資訊都是後設資料。而 ShardingSphere 中的核心功能如資料分片、加解密等都是需要基於資料庫的後設資料生成路由或者加密解密的列實現,由此可見 後設資料是 ShardingSphere 系統執行的核心,同樣也是每一個資料儲存相關中介軟體或者元件的核心資料 。有了後設資料的注入,相當於整個系統有了神經中樞,可以結合後設資料完成對於庫、表、列的個性化操作,如資料分片、資料加密、SQL 改寫等。

而對於 ShardingSphere 後設資料的載入過程,首先需要弄清楚在 ShardingSphere 中後設資料的型別以及分級。在 ShardingSphere 中後設資料主要圍繞著 ShardingSphereMetaData 來進行展開,其中較為核心的是 ShardingSphereSchema。該結構是資料庫的後設資料,同時也為資料來源後設資料的頂層物件,在 ShardingSphere 中資料庫後設資料的結構如下圖。對於每一層來說,上層資料來源於下層資料的組裝,所以下面我們採用從下往上的分層方式進行逐一剖析。


ShardingSphere 資料庫後設資料結構圖

二、ColumMetaData 和 IndexMetaData

ColumMetaData 和 IndexMetaData 是組成 TableMetaData 的基本元素,下面我們分開講述兩種後設資料的結構以及載入過程。ColumMetaData 主要結構如下:


public 
final 

class 
ColumnMetaData {

     // 列名
     private  final String name;
     // 資料型別
     private  final  int dataType;
     // 是否主鍵
     private  final  boolean primaryKey;
     // 是否自動生成
     private  final  boolean generated;
     // 是否區分大小寫
     private  final  boolean caseSensitive;
}

其載入過程主要封裝在  org.apache.shardingsphere.infra.metadata.schema.builder.loader.ColumnMetaDataLoader#load  方法中,主要過程是透過資料庫連結獲取後設資料匹配表名載入某個表名下所有的列的後設資料。核心程式碼如下:


/**

 * Load column meta data list.
 *
 * @param connection connection
 * @param tableNamePattern table name pattern
 * @param databaseType database type
 * @return column meta data list
 * @throws SQLException SQL exception
 */

public  static Collection<ColumnMetaData> load( final Connection connection,  final  String tableNamePattern,  final DatabaseType databaseType) throws SQLException {
    Collection<ColumnMetaData> result =  new LinkedList<>();
    Collection< String> primaryKeys = loadPrimaryKeys(connection, tableNamePattern);
     List< String> columnNames =  new ArrayList<>();
     List<Integer> columnTypes =  new ArrayList<>();
     List< String> columnTypeNames =  new ArrayList<>();
     List<Boolean> isPrimaryKeys =  new ArrayList<>();
     List<Boolean> isCaseSensitives =  new ArrayList<>();
     try (ResultSet resultSet = connection.getMetaData().getColumns(connection.getCatalog(), connection.getSchema(), tableNamePattern,  "%")) {
         while (resultSet.next()) {
             String tableName = resultSet.getString(TABLE_NAME);
             if (Objects.equals(tableNamePattern, tableName)) {
                 String columnName = resultSet.getString(COLUMN_NAME);
                columnTypes.add(resultSet.getInt(DATA_TYPE));
                columnTypeNames.add(resultSet.getString(TYPE_NAME));
                isPrimaryKeys.add(primaryKeys.contains(columnName));
                columnNames.add(columnName);
            }
        }
    }
     try (Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery(generateEmptyResultSQL(tableNamePattern, databaseType))) {
         for ( int i =  0; i < columnNames.size(); i++) {
            isCaseSensitives.add(resultSet.getMetaData().isCaseSensitive(resultSet.findColumn(columnNames. get(i))));
            result.add( new ColumnMetaData(columnNames. get(i), columnTypes. get(i), isPrimaryKeys. get(i),
                    resultSet.getMetaData().isAutoIncrement(i +  1), isCaseSensitives. get(i)));
        }
    }
     return result;
}

IndexMetaData 其實是表中的索引的名稱,所以沒有複雜的結構屬性,只有一個名稱,所以不展開贅述,重點講述一下載入過程。載入過程和 column 類似,主要流程在  org.apache.shardingsphere.infra.metadata.schema.builder.loader.IndexMetaDataLoader#load  方法中,基本流程同樣也是透過資料庫連結獲取相關資料庫和表的後設資料中的 indexInfo 組織核心的 IndexMetaData,實現程式碼如下:



public 
static Collection<IndexMetaData> 
load
(
final Connection connection, 
final String table) 
throws SQLException {

    Collection<IndexMetaData> result =  new HashSet<>();
     try (ResultSet resultSet = connection.getMetaData().getIndexInfo(connection.getCatalog(), connection.getSchema(), table,  falsefalse)) {
         while (resultSet.next()) {
            String indexName = resultSet.getString(INDEX_NAME);
             if ( null != indexName) {
                result.add( new IndexMetaData(indexName));
            }
        }
    }  catch ( final SQLException ex) {
         if (ORACLE_VIEW_NOT_APPROPRIATE_VENDOR_CODE != ex.getErrorCode()) {
             throw ex;
        }
    }
     return result;
}

三、TableMetaData

該型別是組成 ShardingSphereMetaData 的基本元素,其結構如下:

public 
final 

class 
TableMetaData {

     // 表名
    private  final  String name;
     // 列後設資料
    private  final  Map< String, ColumnMetaData> columns;
     // 索引後設資料
    private  final  Map< String, IndexMetaData> indexes;
     //省略一些方法
}

從上述結構可以看出,TableMetaData 其實是由 ColumnMetaData 和 IndexMetaData 組裝而來,所以 TableMetaData 的載入過程可以理解為是一箇中間層,具體的實現還是 ColumnMetaDataLoader 和 IndexMetaDataLoader 拿到表名以及相關連結進行資料載入。所以比較簡單的 TableMetaData 載入過程主要在  org.apache.shardingsphere.infra.metadata.schema.builder.loader.TableMetaDataLoader#load  方法,其載入的核心流程如下:



public 
static Optional<TableMetaData> 
load
(
final DataSource dataSource, 
final String tableNamePattern, 
final DatabaseType databaseType) 
throws SQLException {

     // 獲取連結
     try (MetaDataLoaderConnectionAdapter connectionAdapter =  new MetaDataLoaderConnectionAdapter(databaseType, dataSource.getConnection())) {
         // 根據不同的資料庫型別,格式化表名的模糊匹配欄位
        String formattedTableNamePattern = databaseType.formatTableNamePattern(tableNamePattern);
         // 載入ColumnMetaData和IndexMetaData組裝TableMetaData
         return isTableExist(connectionAdapter, formattedTableNamePattern)
                ? Optional.of( new TableMetaData(tableNamePattern, ColumnMetaDataLoader.load(
                        connectionAdapter, formattedTableNamePattern, databaseType), IndexMetaDataLoader.load(connectionAdapter, formattedTableNamePattern)))
                : Optional.empty();
    }
}

四、SchemaMetaData

經過下兩層的分析,很明顯這一層是後設資料暴露的最外層,最外的層的結構為 ShardingSphereSchema,其主要結構為:


/**

 * ShardingSphere schema.
 */

@Getter
public  final  class  ShardingSphereSchema {

     private  final Map<String, TableMetaData> tables;

     @SuppressWarnings( "CollectionWithoutInitialCapacity")
     public  ShardingSphereSchema () {
        tables =  new ConcurrentHashMap<>();
    }

     public  ShardingSphereSchema ( final Map<String, TableMetaData> tables) {
         this.tables =  new ConcurrentHashMap<>(tables.size(),  1);
        tables.forEach((key, value) ->  this.tables.put(key.toLowerCase(), value));
    }

和 schema 的概念契合,一個 schema 含有若干個表。ShardingSphereSchema 的屬性是一個 map 結構,key 為 tableName,value 是表名對應表的後設資料。主要是透過建構函式完成初始化。所以,還是重點對於表後設資料的載入,下面我們從入口跟進。

整個後設資料載入的核心入口在  org.apache.shardingsphere.infra.context.metadata.MetaDataContextsBuilder#build  中。在 build 中主要是透過配置的規則,組裝和載入相對應的後設資料,核心程式碼如下:


/**

 * Build meta data contexts.
 * 
 * @exception SQLException SQL exception
 * @return meta data contexts
 */

public StandardMetaDataContexts build() throws SQLException {
     Map< String, ShardingSphereMetaData> metaDataMap =  new HashMap<>(schemaRuleConfigs.size(),  1);
     Map< String, ShardingSphereMetaData> actualMetaDataMap =  new HashMap<>(schemaRuleConfigs.size(),  1);
     for ( String each : schemaRuleConfigs.keySet()) {
         Map< String, DataSource> dataSourceMap = dataSources. get(each);
        Collection<RuleConfiguration> ruleConfigs = schemaRuleConfigs. get(each);
        DatabaseType databaseType = DatabaseTypeRecognizer.getDatabaseType(dataSourceMap.values());
         // 獲取配置的規則
        Collection<ShardingSphereRule> rules = ShardingSphereRulesBuilder.buildSchemaRules(each, ruleConfigs, databaseType, dataSourceMap);
         // 載入actualTableMetaData和logicTableMetaData
         Map<TableMetaData, TableMetaData> tableMetaDatas = SchemaBuilder.build( new SchemaBuilderMaterials(databaseType, dataSourceMap, rules, props));
         // 組裝規則後設資料
        ShardingSphereRuleMetaData ruleMetaData =  new ShardingSphereRuleMetaData(ruleConfigs, rules);
         // 組裝資料來源後設資料
        ShardingSphereResource resource = buildResource(databaseType, dataSourceMap);
         // 組裝資料庫後設資料
        ShardingSphereSchema actualSchema =  new ShardingSphereSchema(tableMetaDatas.keySet().stream().filter(Objects::nonNull).collect(Collectors.toMap(TableMetaData::getName, v -> v)));
        actualMetaDataMap.put(each,  new ShardingSphereMetaData(each, resource, ruleMetaData, actualSchema));
        metaDataMap.put(each,  new ShardingSphereMetaData(each, resource, ruleMetaData, buildSchema(tableMetaDatas)));
    }
     // 
    OptimizeContextFactory optimizeContextFactory =  new OptimizeContextFactory(actualMetaDataMap);
     return  new StandardMetaDataContexts(metaDataMap, buildGlobalSchemaMetaData(metaDataMap), executorEngine, props, optimizeContextFactory);
}

透過上述程式碼可以看出在 build 方法中,主要基於配置的 schemarule 載入了資料庫的基本資料如資料庫型別、資料庫連線池等,透過這些資料完成對於 ShardingSphereResource 的組裝;完成 ShardingSphereRuleMetaData 如配置規則、加密規則、認證規則等資料組裝;完成 ShardingSphereSchema 中的必要資料庫後設資料的載入。跟蹤找到表後設資料的載入方法即  org.apache.shardingsphere.infra.metadata.schema.builder.SchemaBuilder#build ,在這個方法中,分別載入了 actualTableMetaData 以及 logicTableMetaData,那麼什麼是 actualTable,什麼是 logicTable 呢?簡單的來說對 於  t_order_1 t_order_2 算是  t_order 的節點,所以在概念上來分析, t_order 是 logicTable,而  t_order_1 和  t_order_2 是 actualTable。明確了這兩個概念後,我們再來一起看 build 方法,主要分為以下兩步:

1. actualTableMetaData 載入

actualTableMetaData 是系統分片的基礎表,在 5.0.0-beta 版本中,我們採用了資料庫方言的方式利用 SQL 進行後設資料的查詢載入,所以基本流程就是首先透過透過 SQL 進行資料庫後設資料的查詢載入,如果沒找到資料庫方言載入器,則採用 JDBC 驅動連線進行獲取,再結合 ShardingSphereRule 中配置的表名,進行配置表的後設資料的載入。核心程式碼如下所示:

private 
static 
Map<
String, TableMetaData> buildActualTableMetaDataMap(final SchemaBuilderMaterials materials) throws SQLException {

     Map< String, TableMetaData> result =  new HashMap<>(materials.getRules().size(),  1);
     // 資料庫方言SQL載入後設資料
    appendRemainTables(materials, result);
     for (ShardingSphereRule rule : materials.getRules()) {
         if (rule  instanceof TableContainedRule) {
             for ( String table : ((TableContainedRule) rule).getTables()) {
                 if (!result.containsKey(table)) {
                    TableMetaDataBuilder.load(table, materials).map(optional -> result.put(table, optional));
                }
            }
        }
    }
     return result;
}

2. logicTableMetaData 載入

由上述的概念可以看出 logicTable 是 actualTable 基於不同的規則組裝而來的實際的邏輯節點,可能是分片節點也可能是加密節點或者是其他,所以 logicTableMetaData 是以 actualTableMetaData 為基礎,結合具體的配置規則如分庫分表規則等關聯的節點。在具體流程上,首先獲取配置規則的表名,然後判斷是否已經載入過 actualTableMetaData,透過 TableMetaDataBuilder#decorate 方法結合配置規則,生成相關邏輯節點的後設資料。核心程式碼流程如下所示:

private 
static 
Map<
String, TableMetaData> buildLogicTableMetaDataMap(
final SchemaBuilderMaterials materials, 
final 
Map<
String, TableMetaData> tables) throws SQLException {

     Map< String, TableMetaData> result =  new HashMap<>(materials.getRules().size(),  1);
     for (ShardingSphereRule rule : materials.getRules()) {
         if (rule instanceof TableContainedRule) {
             for ( String table : ((TableContainedRule) rule).getTables()) {
                 if (tables.containsKey(table)) {
                    TableMetaData metaData = TableMetaDataBuilder.decorate(table, tables. get(table), materials.getRules());
                    result.put(table, metaData);
                }
            }
        }
    }
     return result;
}

至此,核心後設資料載入完成,封裝成一個 Map 進行返回,供各個需求場景進行使用。


後設資料載入最佳化分析

雖然說後設資料是我們系統的核心,是必不可少的,但是在系統啟動時進行資料載入,必然會導致系統的負載增加,系統啟動效率低。所以我們需要對載入的過程進行最佳化,目前主要是以下兩方面的探索:

一、使用 SQL 查詢替換原生 JDBC 驅動連線

在 5.0.0-beta 版本之前,採用的方式是透過原生 JDBC 驅動原生方式載入。在 5.0.0-beta 版本中,我們逐步採用了使用資料庫方言,透過 SQL 查詢的方式,多執行緒方式實現了後設資料的載入。進一步提高了系統資料載入的速度。詳細的方言 Loader 可以檢視  org.apache.shardingsphere.infra.metadata.schema.builder.spi.DialectTableMetaDataLoader  的相關實現。

二、減少後設資料的載入次數

對於系統通用的資源的載入,我們遵循一次載入,多處使用。當然在這個過程中,我們也要權衡空間和時間,所以我們在不斷的進行最佳化,減少後設資料的重複載入,提高系統整體的效率。

更多詳細功能,歡迎下載使用 Apache ShardingSphere 5.0.0-beta。


歡迎大家掃碼關注


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70001955/viewspace-2784739/,如需轉載,請註明出處,否則將追究法律責任。

相關文章