Sharding-JDBC 原始碼之啟動流程分析

sky凌亂的微笑發表於2020-12-20

Sharding-JDBC 是 ShardingSphere 開源的分散式資料庫中介軟體產品之一,提供標準化的資料分片、分散式事務和資料庫治理功能,可適用於如Java同構、異構語言、雲原生等各種多樣化的應用場景。
Sharding-JDBC 在 Java 的 JDBC 層提供額外服務,它使用客戶端直連資料庫,以jar包形式提供服務,無需額外部署和依賴,可理解為增強版的JDBC驅動,完全相容JDBC和各種ORM框架

接下來,讓我們由簡入繁,逐漸拉來 Sharding-JDBC 的序幕。
首先基於 SpringBoot2.4 快速搭建一個 Demo,主要 jar 包依賴版本分別是:

  • io.shardingsphere.sharding-jdbc-spring-boot-starter:3.0.0.M1
  • com.alibaba.druid-spring-boot-starter:1.1.17
  • org.mybatis.spring.boot.mybatis-spring-boot-starter:2.1.4

配置 application.properties :

spring.application.name=shardingjdbcdemo
server.port=8080
spring.main.allow-bean-definition-overriding=true

#資料來源名稱
sharding.jdbc.datasource.names=ds0,ds1
#資料來源 ds0 的配置
sharding.jdbc.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.cj.jdbc.Driver
sharding.jdbc.datasource.ds0.url=jdbc\:mysql\://localhost\:3306/d_sharding_demo1?useUnicode\=true&characterEncoding\=UTF-8&autoReconnect\=true&failOverReadOnly\=false&allowMultiQueries\=true&serverTimezone=GMT
sharding.jdbc.datasource.ds0.username=root
sharding.jdbc.datasource.ds0.password=123456
sharding.jdbc.datasource.ds0.initialSize=1
sharding.jdbc.datasource.ds0.minIdle=1
sharding.jdbc.datasource.ds0.maxActive=50

#資料來源 ds1 的配置
sharding.jdbc.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.cj.jdbc.Driver
sharding.jdbc.datasource.ds1.url=jdbc\:mysql\://localhost\:3306/d_sharding_demo2?useUnicode\=true&characterEncoding\=UTF-8&autoReconnect\=true&failOverReadOnly\=false&allowMultiQueries\=true&serverTimezone=GMT
sharding.jdbc.datasource.ds1.username=root
sharding.jdbc.datasource.ds1.password=123456
sharding.jdbc.datasource.ds1.initialSize=1
sharding.jdbc.datasource.ds1.minIdle=1
sharding.jdbc.datasource.ds1.maxActive=50

#控制檯展示 sql 語句
sharding.jdbc.config.sharding.props.sql.show=true

#實際節點
sharding.jdbc.config.sharding.tables.t_goods.actual-data-nodes=ds$->{0..1}.t_goods_$->{0..1}
#分表列
sharding.jdbc.config.sharding.tables.t_goods.table-strategy.inline.shardingColumn=goods_id
#分片策略
sharding.jdbc.config.sharding.tables.t_goods.table-strategy.inline.algorithm-expression=t_goods_$->{goods_id % 2}

#分庫列
sharding.jdbc.config.sharding.tables.t_goods.databaseStrategy.standard.shardingColumn=type
#分庫策略
sharding.jdbc.config.sharding.tables.t_goods.databaseStrategy.standard..preciseAlgorithmClassName=com.example.shardingjdbcdemo.algorithm.MyPreciseShardingAlgorithm

客戶端測試程式

@Test
public void insert() {
    GoodsDTO order = new GoodsDTO();
    for (int i = 0; i < 1; i++) {
        order.setGoodsId(i + 1L);
        order.setGoodsName("商品" + (i + 1));
        order.setType(0);
        orderInfoMapper.insertGoods(order);
    }
}

原始碼分析

當執行上面的客戶端程式時,Springboot 會去拉取 application.properties 中的配置,下面我們來看下 sharding-jdbc-spring-boot-starter 中的類,
sharding-jdbc-starter

SpringBootShardingRuleConfigurationProperties 獲取配置
@ConfigurationProperties(prefix = "sharding.jdbc.config.sharding")
public class SpringBootShardingRuleConfigurationProperties extends YamlShardingRuleConfiguration {
}

@ConfigurationProperties 註解修飾的類會將配置檔案中的屬性名稱匹配的的值繫結到類的屬性欄位上。

SpringBootConfiguration 獲取資料來源及分庫分表策略
@Configuration
@EnableConfigurationProperties({SpringBootShardingRuleConfigurationProperties.class, SpringBootMasterSlaveRuleConfigurationProperties.class})
public class SpringBootConfiguration implements EnvironmentAware {
    @Autowired
    private SpringBootShardingRuleConfigurationProperties shardingProperties;
    
    /**
     * 1. 實現 EnvironmentAware 介面,獲取配置資訊
     * 2. 根據配置檔案資訊獲取資料來源配置
     */
    @Override
    public void setEnvironment(final Environment environment) {
        setDataSourceMap(environment);
    }
    
    @SuppressWarnings("unchecked")
    private void setDataSourceMap(final Environment environment) {
        String prefix = "sharding.jdbc.datasource.";
        String dataSources = environment.getProperty(prefix + "names");
        // 獲取資料來源並存入 dataSourceMap 中
        for (String each : dataSources.split(",")) {
            try {
                Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, prefix + each, Map.class);
                Preconditions.checkState(!dataSourceProps.isEmpty(), "Wrong datasource properties!");
                DataSource dataSource = DataSourceUtil.getDataSource(dataSourceProps.get("type").toString(), dataSourceProps);
                dataSourceMap.put(each, dataSource);
            } catch (final ReflectiveOperationException ex) {
                throw new ShardingException("Can't find datasource type!", ex);
            }
        }
    }
    
    @Bean
    public DataSource dataSource() throws SQLException {
        // 獲取資料來源資訊
        return null == masterSlaveProperties.getMasterDataSourceName() 
                ? ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingProperties.getShardingRuleConfiguration(), shardingProperties.getConfigMap(), shardingProperties.getProps())
                : MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, masterSlaveProperties.getMasterSlaveRuleConfiguration(), masterSlaveProperties.getConfigMap());
    }
  • @EnableConfigurationProperties 可以使被 @ConfigurationProperties 註解的類生效;
  • 實現 EnvironmentAware 介面,可以通過 Environment 獲取配置檔案中的配置資訊;
  • @Bean 註解的 dataSource 方法會去真正的解析並封裝配置資料,即通過 shardingProperties.getShardingRuleConfiguration()
接下來看如何獲取路由規則配置
public class YamlShardingRuleConfiguration {
    
    /**
     * ...... 省略部分屬性定義
     */
     private Map<String, YamlTableRuleConfiguration> tables = new LinkedHashMap<>();
    
    /**
     * 將 YamlShardingRuleConfiguration 轉為 ShardingRuleConfiguration
     */
    public ShardingRuleConfiguration getShardingRuleConfiguration() {
        ShardingRuleConfiguration result = new ShardingRuleConfiguration();
        result.setDefaultDataSourceName(defaultDataSourceName);
        // 遍歷從配置檔案中繫結的 tables 物件
        for (Entry<String, YamlTableRuleConfiguration> entry : tables.entrySet()) {
            YamlTableRuleConfiguration tableRuleConfig = entry.getValue();
            tableRuleConfig.setLogicTable(entry.getKey());
            // 將構建完的 tableRuleConfig 新增到 tableRuleConfigs 中
            result.getTableRuleConfigs().add(tableRuleConfig.build());
        }
        // 獲取繫結表
        result.getBindingTableGroups().addAll(bindingTables);
        // 獲取預設分庫策略
        if (null != defaultDatabaseStrategy) {
            result.setDefaultDatabaseShardingStrategyConfig(defaultDatabaseStrategy.build());
        }
        // 獲取預設分表策略
        if (null != defaultTableStrategy) {
            result.setDefaultTableShardingStrategyConfig(defaultTableStrategy.build());
        }
        // 獲取預設主鍵生成器
        if (null != defaultKeyGeneratorClassName) {
            result.setDefaultKeyGenerator(KeyGeneratorFactory.newInstance(defaultKeyGeneratorClassName));
        }
        // 獲取主從路由配置
        Collection<MasterSlaveRuleConfiguration> masterSlaveRuleConfigs = new LinkedList<>();
        for (Entry<String, YamlMasterSlaveRuleConfiguration> entry : masterSlaveRules.entrySet()) {
            YamlMasterSlaveRuleConfiguration each = entry.getValue();
            each.setName(entry.getKey());
            masterSlaveRuleConfigs.add(entry.getValue().getMasterSlaveRuleConfiguration());
        }
        result.setMasterSlaveRuleConfigs(masterSlaveRuleConfigs);
        return result;
    }
}

在上面的 getShardingRuleConfiguration 中,關鍵是在 tables 的遍歷上面,會逐一構建各個表的路由規則。

迴圈呼叫 tableRuleConfig.build() 構建表的規則配置:
    public TableRuleConfiguration build() {
        Preconditions.checkNotNull(logicTable, "Logic table cannot be null.");
        // 表配置物件
        TableRuleConfiguration result = new TableRuleConfiguration();
        result.setLogicTable(logicTable);
        result.setActualDataNodes(actualDataNodes);
        // 設定分庫策略
        if (null != databaseStrategy) {
            // 構建分庫策略
            result.setDatabaseShardingStrategyConfig(databaseStrategy.build());
        }
        // 設定分表策略
        if (null != tableStrategy) {
            result.setTableShardingStrategyConfig(tableStrategy.build());
        }
        if (!Strings.isNullOrEmpty(keyGeneratorClassName)) {
            result.setKeyGenerator(KeyGeneratorFactory.newInstance(keyGeneratorClassName));
        }
        result.setKeyGeneratorColumnName(keyGeneratorColumnName);
        result.setLogicIndex(logicIndex);
        return result;
    }

在構建表配置時,會去封裝邏輯表名,實際節點以及分庫分表策略。

構建分庫分表策略databaseStrategy.build()
    public ShardingStrategyConfiguration build() {
        int shardingStrategyConfigCount = 0;
        ShardingStrategyConfiguration result = null;
        // 首先獲取標準策略配置
        if (null != standard) {
            shardingStrategyConfigCount++;
            // 只建立精確分片演算法
            if (null == standard.getRangeAlgorithmClassName()) {
                result = new StandardShardingStrategyConfiguration(standard.getShardingColumn(),
                        ShardingAlgorithmFactory.newInstance(standard.getPreciseAlgorithmClassName(), PreciseShardingAlgorithm.class));
            } else {
                // 建立包含範圍和精確的分片演算法
                result = new StandardShardingStrategyConfiguration(standard.getShardingColumn(),
                        ShardingAlgorithmFactory.newInstance(standard.getPreciseAlgorithmClassName(), PreciseShardingAlgorithm.class),
                        ShardingAlgorithmFactory.newInstance(standard.getRangeAlgorithmClassName(), RangeShardingAlgorithm.class));
            }
            
        }
        // 複雜分片演算法
        if (null != complex) {
            shardingStrategyConfigCount++;
            result = new ComplexShardingStrategyConfiguration(complex.getShardingColumns(), ShardingAlgorithmFactory.newInstance(complex.getAlgorithmClassName(), ComplexKeysShardingAlgorithm.class));
        }
        // 內聯表示式分片演算法
        if (null != inline) {
            shardingStrategyConfigCount++;
            result = new InlineShardingStrategyConfiguration(inline.getShardingColumn(), inline.getAlgorithmExpression());
        }
        // 暗示分片演算法,需要根據業務場景手工設定
        if (null != hint) {
            shardingStrategyConfigCount++;
            result = new HintShardingStrategyConfiguration(ShardingAlgorithmFactory.newInstance(hint.getAlgorithmClassName(), HintShardingAlgorithm.class));
        }
        if (null != none) {
            shardingStrategyConfigCount++;
            result = new NoneShardingStrategyConfiguration();
        }
        // 斷言分庫或分表最多隻能含有一個演算法,配置多個分片演算法將丟擲異常
        Preconditions.checkArgument(shardingStrategyConfigCount <= 1, "Only allowed 0 or 1 sharding strategy configuration.");
        return result;
    }

通過以上步驟,就已經將分庫分表的所有配置資訊封裝完成,接下來將會初始化 ShardingDataSource,在初始化 ShardingDataSource 時會校驗資料庫表欄位索引完成性及建立 ShardingContext

校驗資料庫完成性

回到 SpringBootConfigurationdataSource 方法上

    @Bean
    public DataSource dataSource() throws SQLException {
        return null == masterSlaveProperties.getMasterDataSourceName() 
                ? ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingProperties.getShardingRuleConfiguration(), shardingProperties.getConfigMap(), shardingProperties.getProps())
                : MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, masterSlaveProperties.getMasterSlaveRuleConfiguration(), masterSlaveProperties.getConfigMap());
    }

在執行 ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingProperties.getShardingRuleConfiguration(), shardingProperties.getConfigMap(), shardingProperties.getProps()) 時會去初始化 ShardingDataSource,接下來看下 ShardingDataSource 的建構函式:

    public ShardingDataSource(final Map<String, DataSource> dataSourceMap, final ShardingRule shardingRule, final Map<String, Object> configMap, final Properties props) throws SQLException {
        super(dataSourceMap.values());
        if (!configMap.isEmpty()) {
            ConfigMapContext.getInstance().getShardingConfig().putAll(configMap);
        }
        shardingProperties = new ShardingProperties(null == props ? new Properties() : props);
        // 從環境變數中獲取工作執行緒數,預設是虛擬機器的處理器數量
        int executorSize = shardingProperties.getValue(ShardingPropertiesConstant.EXECUTOR_SIZE);
        // 建立執行引擎執行緒池
        executorEngine = new ExecutorEngine(executorSize);
        ShardingMetaData shardingMetaData = new JDBCShardingMetaData(dataSourceMap, shardingRule, getDatabaseType());
        // 初始化 ShardingMetaData 並進行校驗
        shardingMetaData.init(shardingRule);
        boolean showSQL = shardingProperties.getValue(ShardingPropertiesConstant.SQL_SHOW);
        // 建立上下文
        shardingContext = new ShardingContext(dataSourceMap, shardingRule, getDatabaseType(), executorEngine, shardingMetaData, showSQL);
    }

ShardingDataSource 建構函式中會進行一系列的初始化,其中包括 init 方法:

    public void init(final ShardingRule shardingRule) throws SQLException {
        tableMetaDataMap = new HashMap<>(shardingRule.getTableRules().size(), 1);
        for (TableRule each : shardingRule.getTableRules()) {
            refresh(each, shardingRule);
        }
    }

init 方法會迴圈遍歷 tableRules,獲取表的後設資料,具體實現在 refresh 中,refresh 方法會呼叫 getTableMetaData 方法來獲取表後設資料並進行表結構對比:

    private TableMetaData getTableMetaData(final String logicTableName, final List<DataNode> actualDataNodes,
                                           final ShardingDataSourceNames shardingDataSourceNames, final Map<String, Connection> connectionMap) throws SQLException {
        Collection<ColumnMetaData> result = null;
        for (DataNode each : actualDataNodes) {
            Collection<ColumnMetaData> columnMetaDataList = getColumnMetaDataList(each, shardingDataSourceNames, connectionMap);
            // 將第一次迴圈獲得的 columnMetaDataList 設定給 result            
            if (null == result) {
                result = columnMetaDataList;
            }
            // 並以第一次的作為參照與其他分表進行對比
            if (!result.equals(columnMetaDataList)) {
                // 當表結構不一致時丟擲異常
                throw new ShardingException(getErrorMsgOfTableMetaData(logicTableName, result, columnMetaDataList));
            }
        }
        return new TableMetaData(result);
    }

那麼是根據什麼判斷表結構不一致的呢?我們看下 ColumnMetaData 有哪些元素便知:

public final class ColumnMetaData {
    
    private final String columnName;
    
    private final String columnType;
    
    private final String keyType;
}
  1. ColumnMetaData 的資料結構上,我們看到,只有當列名稱、列型別(包括長度)、索引型別完全一致時,才認為表結構是完成的。
  2. 在獲得表的後設資料後,將後設資料封裝在ShardingMetaData.tableMetaDataMap 中並依次返,至此 ShardingDataSource 建立完成,Sharding-JDBC 的啟動流程已全部完成。

總結

  1. Sharding-JDBC 啟動時主要是拉取分庫分表的配置,包括資料來源、分片策略,並校驗配置是否有誤;
  2. 同時,會校驗邏輯表在實際節點的表結構是否一致,通過列名稱、列型別與 Key 型別進行判斷,全部相同時才認為表結構一致;
  3. 封裝表後設資料並建立 ShardingDataSource 資料來源物件。

擴充套件:Mysql 表結構中的 Key 有哪些及含義是什麼?

Key 的含義:表示該列是否被索引

Key 型別含義
“”該列沒有索引或屬於組合索引中的輔助索引
PRI該列是主鍵索引或是組合主鍵索引之一的列
UNI該列是唯一索引的第一列(唯一索引允許空值)
MUL該列是非唯一索引的第一列,也就是普通索引
  • 如果多個 Key 型別應用於表的給定列,則 Key 按 PRIUNIMUL; 的順序顯示優先順序最高的一個;
  • 如果一個唯一索引不能包含空值並且表中沒有主鍵,那麼它可以顯示為PRI
  • 如果多個列組成一個複合唯一索引,則唯一索引可能顯示為MUL,儘管這些列的組合是唯一的,但每列仍然可以包含給定值的多個引用。

相關文章