【踩坑记录】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版配置项说明

分库分表:中间件方案对比

分库分表中间件技术选型总结

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