DIY 3 種分庫分表分片算法,自己寫的輪子才吊!

大家好,我是小富~

前言

本文是《ShardingSphere5.x分庫分表原理與實戰》系列的第六篇,書接上文實現三種自定義分片算法。通過自定義算法,可以根據特定業務需求定製分片策略,以滿足不同場景下的性能、擴展性或數據處理需求。同時,可以優化分片算法以提升系統性能,規避數據傾斜等問題。

在這裏,自定義分片算法的類型(Type)統一爲CLASS_BASED,包含兩個屬性:strategy 表示分片策略類型,目前支持三種:STANDARDCOMPLEXHINTalgorithmClassName 表示自定義分片算法的實現類路徑。此外,還可以向算法類內傳入自定義屬性。

自定義 STANDARD 算法

要實現自定義 STANDARD 標準算法,需要實現StandardShardingAlgorithm<T>接口( T 代表接收的分片健值類型),並重寫接口中的四個方法。其中,有兩個 doSharding() 方法爲處理分片的核心邏輯;getProps() 方法用於獲取分片算法的配置信息;init() 方法則用於初始化分片算法的配置信息,支持動態修改。

5.X 以後的版本,實現自定義標準算法的精準分片和範圍分片,不在需要實現多個接口。只用實現 StandardShardingAlgorithm 標準算法接口,重寫兩個 doSharding() 方法。 doSharding(availableTargetNames,rangeShardingValue) 處理含有 >、<、between and 等操作符的 SQL,doSharding(availableTargetNames,preciseShardingValue) 處理含有 = 、in 等操作符的 SQL。

精準分片

精準分片用於SQL中包含 in、= 等操作符的場景,支持單一分片健。

重寫方法 doSharding(Collection availableTargetNames, PreciseShardingValue preciseShardingValue),該方法返回單一的分片數據源或分片表數據。有兩個參數:一個是可用目標分庫、分表的集合,另一個是精準分片屬性對象。

PreciseShardingValue 對象屬性數據格式如下:

{
  "columnName": "order_id", // 分片健
  "dataNodeInfo": {  
    "paddingChar": "0",
    "prefix": "db",   // 數據節點信息前綴,例如:分庫時爲db,分表時爲分片表t_order_
    "suffixMinLength": 1
  },
  "logicTableName": "t_order", // 邏輯表
  "value": 1 // 分片健值
}

範圍分片

範圍分片用於 SQL中包含 >、< 等範圍操作符的場景,支持單一分片健。

重寫方法 doSharding(Collection availableTargetNames, RangeShardingValue rangeShardingValue),該方法可以返回多個分片數據源或分片表數據。有兩個參數:一個是可用目標分庫、分表的集合,另一個是精準分片屬性對象。

RangeShardingValue 對象屬性數據格式如下:

{
  "columnName": "order_id", // 分片健
  "dataNodeInfo": {
    "paddingChar": "0",
    "prefix": "db",  // 數據節點前綴,分庫時爲數據源,分表時爲分片表t_order_
    "suffixMinLength": 1
  },
  "logicTableName": "t_order", // 邏輯表
  "valueRange": [0,∞]  // 分片健值的範圍數據
}

精準分片算法的 doSharding() 執行流程:從PreciseShardingValue.getValue()中獲取分片鍵值,然後經過計算得出相應編號,最終在availableTargetNames可用目標分庫、分片表集合中選擇以一個符合的返回。

範圍分片算法的 doSharding() 執行流程:從RangeShardingValue.getValueRange()方法獲取分片鍵的數值範圍,然後經過計算得出相應編號,最終在availableTargetNames可用目標分庫、分片表集合中選擇多個符合的返回。

下面是具體實現分片的邏輯:

/**
 * 自定義標準分片算法
 *
 * @author 公衆號:程序員小富
 * @date 2024/03/22 11:02
 */
@Slf4j
public class OrderStandardCustomAlgorithm implements StandardShardingAlgorithm<Long> {

    /**
     * 精準分片進入 sql中有 = 和 in 等操作符會執行
     *
     * @param availableTargetNames 所有分片表的集合
     * @param shardingValue        分片健的值,SQL中解析出來的分片值
     */
    @Override
    public String doSharding(Collection<String> availableTargetNames,
                             PreciseShardingValue<Long> shardingValue) {
        /**
         * 分庫策略使用時:availableTargetNames 參數數據爲分片庫的集合 ["db0","db1"]
         * 分表策略使用時:availableTargetNames 參數數據爲分片庫的集合 ["t_order_0","t_order_1","t_order_2"]
         */
        log.info("進入精準分片 precise availableTargetNames:{}", JSON.toJSONString(availableTargetNames));

        /**
         * 分庫策略使用時: shardingValue 參數數據:{"columnName":"order_id","dataNodeInfo":{"paddingChar":"0","prefix":"db","suffixMinLength":1},"logicTableName":"t_order","value":1}
         * 分表策略使用時: shardingValue 參數數據:{"columnName":"order_id","dataNodeInfo":{"paddingChar":"0","prefix":"t_order_","suffixMinLength":1},"logicTableName":"t_order","value":1}
         */
        log.info("進入精準分片 preciseShardingValue:{}", JSON.toJSONString(shardingValue));
        int tableSize = availableTargetNames.size();
        // 真實表的前綴
        String tablePrefix = shardingValue.getDataNodeInfo().getPrefix();
        // 分片健的值
        long orderId = shardingValue.getValue();
        // 對分片健取模後確定位置
        long mod = orderId % tableSize;
        return tablePrefix + mod;
    }

    /**
     * 範圍分片進入 sql中有 between 和  < > 等操作符會執行
     *
     * @param availableTargetNames 所有分片表的集合
     * @param shardingValue        分片健的值,SQL中解析出來的分片值
     * @return
     */
    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames,
                                         RangeShardingValue<Long> shardingValue) {
        /**
         * 分庫策略使用時:availableTargetNames 參數數據爲分片庫的集合 ["db0","db1"]
         * 分表策略使用時:availableTargetNames 參數數據爲分片庫的集合 ["t_order_0","t_order_1","t_order_2"]
         */
        log.info("進入範圍分片:range availableTargetNames:{}", JSON.toJSONString(availableTargetNames));


        /**
         * 分庫策略使用時 shardingValue 參數數據:{"columnName":"order_id","dataNodeInfo":{"paddingChar":"0","prefix":"db","suffixMinLength":1},"logicTableName":"t_order","valueRange":{"empty":false}}
         * 分表策略使用時 shardingValue 參數數據:{"columnName":"order_id","dataNodeInfo":{"paddingChar":"0","prefix":"t_order_","suffixMinLength":1},"logicTableName":"t_order","valueRange":{"empty":false}}
         */
        log.info("進入範圍分片:rangeShardingValue:{}", JSON.toJSONString(shardingValue));
        // 分片健值的下邊界
        Range<Long> valueRange = shardingValue.getValueRange();
        Long lower = valueRange.lowerEndpoint();
        // 分片健值的上邊界
        Long upper = valueRange.upperEndpoint();
        // 真實表的前綴
        String tablePrefix = shardingValue.getDataNodeInfo().getPrefix();
        if (lower != null && upper != null) {
            // 分片健的值
            long orderId = upper - lower;
            // 對分片健取模後確定位置
            long mod = orderId % availableTargetNames.size();
            return Arrays.asList(tablePrefix + mod);
        }
        //
        return Collections.singletonList("t_order_0");
    }

    @Override
    public Properties getProps() {
        return null;
    }

    /**
     * 初始化配置
     *
     * @param properties
     */
    @Override
    public void init(Properties properties) {
        Object prop = properties.get("prop");
        log.info("配置信息:{}", JSON.toJSONString(prop));
    }
}

配置算法

在實現了自定義分片算法的兩個 doSharding() 核心邏輯之後,接着配置並使用定義的算法。配置屬性包括strategy分片策略類型設置成standardalgorithmClassName自定義標準算法的實現類全路徑。需要注意的是:策略和算法類型必須保持一致,否則會導致錯誤

spring:
  shardingsphere:
    rules:
      sharding:
        # 分片算法定義
        sharding-algorithms:
          t_order_database_mod:
            type: MOD
            props:
              sharding-count: 2 # 指定分片數量
          # 12、自定義 STANDARD 標準算法
          t_order_standard_custom_algorithm:
            type: CLASS_BASED
            props:
              # 分片策略
              strategy: standard
              # 分片算法類
              algorithmClassName: com.shardingsphere_101.algorithm.OrderStandardCustomAlgorithm
              # 自定義屬性
              prop:
                aaaaaa: 123456
                bbbbbb: 654321
        tables:
          # 邏輯表名稱
          t_order:
            # 數據節點:數據庫.分片表
            actual-data-nodes: db$->{0..1}.t_order_${0..2}
            # 分庫策略
            database-strategy:
              standard:
                sharding-column: order_id
                sharding-algorithm-name: t_order_database_mod
            # 分表策略
            table-strategy:
              standard:
                sharding-column: order_id
                sharding-algorithm-name: t_order_standard_custom_algorithm

測試算法

在插入測試數據時,默認會自動進入精確分片的 doSharding() 方法內,看到該方法會獲取分片鍵的數值,根據我們的計算規則確定返回一個目標分片表用於路由。

接着執行一個範圍查詢的 SQL,此時將進入範圍分片的 doSharding() 方法。通過觀察 shardingValue.getValueRange() 方法中分片鍵的數值範圍,可以發現這些數值範圍是從SQL查詢中解析得到的。

select * from t_order where order_id > 1 and order_id < 10

自定義 COMPLEX 算法

複合分片算法支持包含 >,>=, <=,<,=,IN 和 BETWEEN AND 等操作符的SQL,支持多分片健。

自定義COMPLEX複合分片算法,需要我們實現 ComplexKeysShardingAlgorithm<T> 接口(其中 T 代表接收的分片鍵值類型),並重寫該接口內部的 3 個方法。其中,主要關注用於處理核心分片邏輯的 doSharding()方法,可以返回多個分片數據源或分片表數據;其他兩個配置方法與上述類似,這裏不再贅述。

重寫複合分片方法 doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValues) 實現定製的多分片健邏輯,該方法有兩個參數:一個是可用目標分庫、分表的集合;另一個是多分片健屬性對象。

logicTableName爲邏輯表名,columnNameAndShardingValuesMap用於存儲多個分片鍵和對應的鍵值,columnNameAndRangeValuesMap用於存儲多個分片鍵和對應的鍵值範圍。

ComplexKeysShardingValue數據結構如下:

public final class ComplexKeysShardingValue<T extends Comparable<?>> implements ShardingValue {
    // 邏輯表
    private final String logicTableName;
    // 多分片健及其數值
    private final Map<String, Collection<T>> columnNameAndShardingValuesMap;
    // 多分片健及其範圍數值
    private final Map<String, Range<T>> columnNameAndRangeValuesMap;
}

核心流程:通過循環 Map 得到多個分片健值進行計算,從 availableTargetNames 可用目標分庫、分片表集合中選擇多個符合條件的返回。

/**
 * 自定義複合分片算法
 *
 * @author 公衆號:程序員小富
 * @date 2024/03/22 11:02
 */
@Slf4j
public class OrderComplexCustomAlgorithm implements ComplexKeysShardingAlgorithm<Long> {

    /**
     * 複合分片算法進入,支持>,>=, <=,<,=,IN 和 BETWEEN AND 等操作符
     *
     * @param availableTargetNames 所有分片表的集合
     * @param complexKeysShardingValue        多個分片健的值,並SQL中解析出來的分片值
     */
    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames,
                                         ComplexKeysShardingValue<Long> complexKeysShardingValue) {

        /**
         * 分庫策略使用時:availableTargetNames 參數數據爲分片庫的集合 ["db0","db1"]
         * 分表策略使用時:availableTargetNames 參數數據爲分片庫的集合 ["t_order_0","t_order_1","t_order_2"]
         */
        log.info("進入複合分片:complex availableTargetNames:{}", JSON.toJSONString(availableTargetNames));

        // 多分片健和其對應的分片健範圍值
        Map<String, Range<Long>> columnNameAndRangeValuesMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();
        log.info("進入複合分片:columnNameAndRangeValuesMap:{}", JSON.toJSONString(columnNameAndRangeValuesMap));

        columnNameAndRangeValuesMap.forEach((columnName, range) -> {
            // 分片健
            log.info("進入複合分片:columnName:{}", columnName);
            // 分片健範圍值
            log.info("進入複合分片:range:{}", JSON.toJSONString(range));
        });

        // 多分片健和其對應的分片健值
        Map<String, Collection<Long>> columnNameAndShardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
        log.info("進入複合分片:columnNameAndShardingValuesMap:{}", JSON.toJSONString(columnNameAndShardingValuesMap));
        columnNameAndShardingValuesMap.forEach((columnName, shardingValues) -> {
            // 分片健
            log.info("進入複合分片:columnName:{}", columnName);
            // 分片健值
            log.info("進入複合分片:shardingValues:{}", JSON.toJSONString(shardingValues));
        });

        return null;
    }
}

配置算法

處理完複合分片算法的doSharding()核心邏輯,接着配置使用定義的算法,配置屬性包括strategy分片策略類型設置成complexalgorithmClassName自定義算法的實現類全路徑。

需要注意:配置分片鍵時,一定要使用 sharding-columns 表示複數形式,很容易出錯。

spring:
  shardingsphere:
    rules:
      sharding:
        sharding-algorithms:
          t_order_database_mod:
            type: MOD
            props:
              sharding-count: 2 # 指定分片數量
          # 13、自定義 complex 標準算法
          t_order_complex_custom_algorithm:
            type: CLASS_BASED
            props:
              # 分片策略
              strategy: complex
              # 分片算法類
              algorithmClassName: com.shardingsphere_101.algorithm.OrderComplexCustomAlgorithm
              # 自定義屬性
              aaaaaa: aaaaaa
        tables:
          # 邏輯表名稱
          t_order:
            # 數據節點:數據庫.分片表
            actual-data-nodes: db$->{0..1}.t_order_${0..2}
            # 分庫策略
            database-strategy:
              standard:
                sharding-column: order_id
                sharding-algorithm-name: t_order_database_mod
            # 分表策略
            table-strategy:
              complex:
                sharding-columns: order_id , user_id
                sharding-algorithm-name: t_order_complex_custom_algorithm

測試算法

插入測試數據,debug 進入 doSharding() 方法,看到columnNameAndShardingValuesMap內獲取到了 user_id 、order_id 兩個分片鍵及健值。

當執行範圍查詢的SQL,columnNameAndRangeValuesMap屬性內獲取到了 user_id、order_id 兩個分片鍵及健值範圍,通過range.upperEndpoint()、lowerEndpoint()得到上下界值。

select * from t_order where order_id > 1 and user_id > 1;

自定義 HINT 算法

要實現自定義HINT強制路由分片算法,需要實現 HintShardingAlgorithm<T> 接口( T 代表接收的分片鍵值類型)。在實現過程中,需要重寫接口中的3個方法。其中,核心的分片邏輯在 doSharding() 方法中處理,可以支持返回多個分片數據源或分片表數據。另外,其他兩個prop配置方法的使用方式與上述相同,這裏不贅述。

重寫 HINT 核心分片方法 doSharding(Collection availableTargetNames, HintShardingValue shardingValue),以實現我們的定製邏輯。該方法接受兩個參數:一個是可用目標分庫、分表的集合,另一個是 Hint 分片屬性對象。

方法內執行流程:我們首先獲取 HintManager API 設置的分庫或分表的分片值,經過計算後得到合適的分片數據源或分片表集合,然後直接路由到目標位置,無需再關注SQL本身的條件信息。

/**
 * 自定義強制路由分片算法
 *
 * @author 公衆號:程序員小富
 * @date 2024/03/22 11:02
 */
@Slf4j
public class OrderHintCustomAlgorithm implements HintShardingAlgorithm<Long> {

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<Long> hintShardingValue) {

        /**
         * 獲取到設置的分表或者分庫的分片值
         * 指定分表時的分片值  hintManager.addTableShardingValue("t_order",2L);
         * 指定分庫時的分片值  hintManager.addDatabaseShardingValue("t_order", 100L);
         */
        Collection<Long> values = hintShardingValue.getValues();
        Collection<String> result = new ArrayList<>();
        // 從所有分片表中得到合適的分片表
        for (String each : availableTargetNames) {
            for (Long value : values) {
                Long mod = value % availableTargetNames.size();
                if (each.endsWith(String.valueOf(mod))) {
                    result.add(each);
                }
            }
        }
        return result;
    }
}

配置算法

配置自定義Hint算法,配置屬性包括strategy分片策略類型設置成hintalgorithmClassName自定義Hint算法的實現類全路徑。使用該算法時無需指定分片健!

spring:
  shardingsphere:
    # 具體規則配置
    rules:
      sharding:
        # 分片算法定義
        sharding-algorithms:
          t_order_database_mod:
            type: MOD
            props:
              sharding-count: 2 # 指定分片數量
          # 14、自定義 hint 標準算法
          t_order_hint_custom_algorithm:
            type: CLASS_BASED
            props:
              # 分片策略
              strategy: hint
              # 分片算法類
              algorithmClassName: com.shardingsphere_101.algorithm.OrderHintCustomAlgorithm
              # 自定義屬性
              bbbbbb: bbbbbb
        tables:
          # 邏輯表名稱
          t_order:
            # 數據節點:數據庫.分片表
            actual-data-nodes: db$->{0..1}.t_order_${0..2}
            # 分庫策略
            database-strategy:
              hint:
                sharding-algorithm-name: t_order_database_mod
            # 分表策略
            table-strategy:
              hint:
                sharding-algorithm-name: t_order_hint_custom_algorithm

測試算法

在執行SQL操作之前,使用 HintManager APIaddDatabaseShardingValueaddTableShardingValue方法來指定分庫或分表的分片值,這樣算法內通過 HintShardingValue 可以獲取到分片值。注意:如果在執行 SQL 時沒有使用 HintManager 指定分片值,那麼執行SQL將會執行全庫表路由

@DisplayName("Hint 自動義分片算法-範圍查詢")
@Test
public void queryHintTableTest() {

    HintManager hintManager = HintManager.getInstance();
    // 指定分表時的分片值
    hintManager.addTableShardingValue("t_order",2L);
    // 指定分庫時的分片值
    hintManager.addDatabaseShardingValue("t_order", 100L);

    QueryWrapper<OrderPo> queryWrapper = new QueryWrapper<OrderPo>()
            .eq("user_id", 20).eq("order_id", 10);
    List<OrderPo> orderPos = orderMapper.selectList(queryWrapper);
    log.info("查詢結果:{}", JSON.toJSONString(orderPos));
}

到這關於 shardingsphere-jdbc 的 3種自定義分片算法實現就全部結束了。

總結

本文介紹了 STANDARD、COMPLEX 和 HINT 三種自定義分片算法的實現,和使用過程中一些要注意的事項。ShardingSphere 內置的十幾種算法,其實已經可以滿足我們絕大部分的業務場景,不過,如果考慮到後續的性能優化和擴展性,定製分片算法是個不錯的選擇。

全部demo案例 GitHub 地址:https://github.com/chengxy-nds/Springboot-Notebook/tree/master/shardingsphere101/shardingsphere-algorithms

我是小富~ 下期見

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章