【踩坑記錄】Sharding-JDBC(3.0.0)之單庫分表

一、背景

        最近的應用場景中,遇到了單表數據量太大,影響效率,分表的情況。所以就出現了“單庫分表”這個需求。一開始我是自己寫的工具類,但是這樣業務代碼就不簡潔,每次CRUD操作之前都要自己計算表名。更嚴重的問題是我沒有考慮到在關聯表中主鍵重複這個問題,會導致業務異常。第一考慮的是每張表的主鍵分段,但是這樣就要經常關注數據庫的情況,萬一預估的數據量不準,又回出現主鍵重複。身邊的同學有用過sharding-jdbc的,反映還不錯,所以我就開始了踩坑之路。

二、理論

         sharding-jdbc可以保證全局主鍵唯一。

         sharding-jdbc表的分片策略有四種,我的情況是根據兩個字段分表,也就是多分片鍵,所以要使用complex模式,這個比標準的分片場景稍微複雜一點,要自己寫分片策略。

          更多的理論知識請查看參考文檔,因爲我是一個坑貨,沒辦法和您解釋。

三、實踐

           第一,引入依賴

        <dependency>
            <groupId>io.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>

           第二,數據庫、分表配置          

# application-test.yml 測試環境DB配置
sharding:
  jdbc:
    datasource: #配置數據源
      user:
        url: 
        username:
        password:
    config:
      sharding:
        props:
          sql:
            show: true #打印SQL

# application.yml 公共配置
sharding:
  jdbc:
    datasource:
      names: user #數據源
      user:#數據庫配置
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        initial-size: 5
        min-idle: 5
        max-active: 100
        max-wait: 10000
        validation-query: SELECT 1 FROM DUAL
        test-on-borrow: false
        test-on-return: false
        test-while-idle: true
        time-between-eviction-runs-millis: 30000
        min-evictable-idle-time-millis: 30000
    config:
      sharding:
        tables:#表的配置
          user_base_info:#邏輯表名
            actual-data-nodes: user.user_base_info$->{2019..2022}_0$->{0..7}#真實的表名
            table-strategy.complex.sharding-columns: finance_year,hash_code#分表字段
            table-strategy.complex.algorithm-class-name: #自定義分表策略實現類,要實現ComplexKeysShardingAlgorithm接口
            key-generator-column-name: id#主鍵
        default-data-source-name: user #默認數據庫

           第三,分表策略實現

import io.shardingsphere.api.algorithm.sharding.ListShardingValue;
import io.shardingsphere.api.algorithm.sharding.ShardingValue;
import io.shardingsphere.api.algorithm.sharding.complex.ComplexKeysShardingAlgorithm;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;


public class WalletComplexKeysShardingAlgorithmImpl implements ComplexKeysShardingAlgorithm {

    private static String logicTableName = "";

    /**
     * 自定義分片策略
     * @param collection    實際表名集合
     * @param shardingValues    分片鍵集合
     * @return
     */
    @Override
    public Collection<String> doSharding(Collection<String> collection, Collection<ShardingValue> shardingValues) {
        Collection<Integer> financeYearValues = getShardingValue(shardingValues, FINANCE_YEAR);
        Collection<Integer> hashCodeValues = getShardingValue(shardingValues, HASH_CODE);

        List<String> shardingSuffix = new ArrayList<>();

        for (Integer financeYear : financeYearValues) {
            for (Integer hashCode : hashCodeValues) {
                long numSuffix = 0L;
                if (logicTableName.equals(TableTypeEnum.WALLET.getCode())) {
                    numSuffix = Math.abs(hashCode) % 8;
                }

                if (logicTableName.equals(TableTypeEnum.WALLET_DETAIL.getCode())
                        || logicTableName.equals(TableTypeEnum.WALLET_ACTION.getCode())
                        || logicTableName.equals(TableTypeEnum.WALLET_ACTION_REL.getCode())
                        || logicTableName.equals(TableTypeEnum.WALLET_DETAIL_BIZ_REL.getCode())) {
                    numSuffix = Math.abs(hashCode) % 16;

                }

                String tableSuffix = "";
                if (numSuffix < 10) {
                    tableSuffix = financeYear + "_0" + numSuffix;
                } else {
                    tableSuffix = financeYear + "_" + numSuffix;
                }

                for (String tableName : collection) {
                    if (tableName.endsWith(tableSuffix)) {
                        shardingSuffix.add(tableName);
                    }
                }
            }
        }

        return shardingSuffix;
    }

    /**
     * 獲得分片鍵的值
     * @param shardingValues    分片鍵集合
     * @param splitKey   分片鍵
     * @return
     */
    private Collection<Integer> getShardingValue(Collection<ShardingValue> shardingValues, final String splitKey) {
        Collection<Integer> valueSet = new ArrayList<>();
        for (ShardingValue shardingValue : shardingValues) {
            if (shardingValue instanceof ListShardingValue) {
                ListShardingValue listShardingValue = (ListShardingValue) shardingValue;
                if (listShardingValue.getColumnName().equals(splitKey)) {
                    logicTableName = listShardingValue.getLogicTableName();
                    return listShardingValue.getValues();
                }
            }
        }

        return valueSet;
    }
}

           第四,踩坑          

           聊一聊血淚史,踩坑踩了兩天!

  • 第一、多分片鍵要使用complex模式,我一開始沒注意,被inline表達式折磨了很久
  • 第二、如果配置的不對,它找不到真正的表,它就會執行所有表
  • 第三、大小寫敏感,如果分片鍵,表中字段是小寫,配置文件中是大寫,恭喜你,它沒辦法找到真正的表
  • 第四、相同的邏輯表的真實表,必須結構相同。啓動應用的時候,加載配置文件,它會檢查配置的分表表結構是否一致,比如你的數據庫中只有2019年的表,而你配置的是2019-2022年的表,很好,工程會起不來,拋異常表結構不一致。
  • 第五、批量insert,不能foreach insert語句,只能foreach values。這樣就會有一個問題,必須指明字段。如果我沒有說清楚,請看代碼

# 不支持的寫法

<insert id="batchInsertSelective" parameterType="java.util.Map">
    <foreach collection="userList" index="index" item="userDO" separator=";">
      insert into user
      <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="userDO.id != null">
          id,
        </if>
        <if test="userDO.createDatetime != null">
          create_datetime,
        </if>
        <if test="userDO.updateDatetime != null">
          update_datetime,
        </if>
        <if test="userDO.createUser != null">
          create_user,
        </if>
        <if test="userDO.updateUser != null">
          update_user,
        </if>
        <if test="userDO.financeYear != null">
          finance_year,
        </if>
        <if test="userDO.hashCode != null">
          hash_code,
        </if>
      </trim>
      <trim prefix="values (" suffix=")" suffixOverrides=",">
        <if test="userDO.id != null">
          #{userDO.id,jdbcType=BIGINT},
        </if>
        <if test="userDO.createDatetime != null">
          #{userDO.createDatetime,jdbcType=TIMESTAMP},
        </if>
        <if test="userDO.updateDatetime != null">
          #{userDO.updateDatetime,jdbcType=TIMESTAMP},
        </if>
        <if test="userDO.createUser != null">
          #{userDO.createUser,jdbcType=VARCHAR},
        </if>
        <if test="userDO.updateUser != null">
          #{userDO.updateUser,jdbcType=VARCHAR},
        </if>
        <if test="userDO.financeYear != null">
          #{userDO.financeYear,jdbcType=INTEGER},
        </if>
        <if test="userDO.hashCode != null">
          #{userDO.hashCode,jdbcType=INTEGER},
        </if>
      </trim>
    </foreach>
  </insert>

# 支持的寫法
  <insert id="batchInsertSelective" parameterType="java.util.List">
    insert into wallet_action_rel (
      create_user, update_user,finance_year,hash_code
    ) values
    <foreach collection="list" index="index" item="userDO" separator=",">
      <trim prefix="(" suffix=")" >
        #{userDO.createUser,jdbcType=VARCHAR},
        #{userDO.updateUser,jdbcType=VARCHAR},
        #{userDO.financeYear,jdbcType=INTEGER},
        #{userDO.hashCode,jdbcType=INTEGER}
      </trim>
    </foreach>
  </insert>
  • 第六、select和update語句,必須把分片鍵寫入到where條件中,否則就操作所有的表。這裏就要注意SQL的效率,分片鍵最好建索引,否則很影響查詢效率。

  • 更多配置,請移步官網,我用的是SpringBoot配置

四、總結

之前看過一個大佬說的話,覺得很有道理,"能不分表就不分表,能用分區表就不要用物理分表"。sharding-jdbc還有很多限制,因爲我的應用場景比較簡單,所以目前還沒有遇到,僅供參考。

 

膜拜各路大佬:

MySQL多數據源筆記5-ShardingJDBC實戰

【分庫分表】sharding-jdbc—分片策略

數據庫主鍵設置爲全局唯一方案

Sharding-jdbc的SQL支持情況總結

關於Sharding-jdbc

sharding-JDBC分庫分表開坑和填坑

sharding-jdbc springboot配置

Spring Boot中整合Sharding-JDBC單庫分表示例 (第二篇)

sharding-jdbc的SpringBoot版配置項說明

分庫分表:中間件方案對比

分庫分表中間件技術選型總結

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