分库分表-订单服务

配置

引入 ShardingSphere 的相关依赖

<properties>
	<shardingsphere.version>5.3.2</shardingsphere.version>
</properties>
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>${shardingsphere.version}</version>
    <exclusions>
        <exclusion>
            <artifactId>logback-classic</artifactId>
            <groupId>ch.qos.logback</groupId>
        </exclusion>
    </exclusions>
</dependency>

根据规则进行分库分表的规则配置

节目项目相关配置:

spring:
  datasource:
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    url: jdbc:shardingsphere:classpath:shardingsphere-order.yaml

shardingsphere-order.yaml配置:

dataSources: 
  # 第一个订单库
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/damai_order_0?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai&autoReconnect=true
    username: root
    password: root
  # 第二个订单库
  ds_1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/damai_order_1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai&autoReconnect=true
    username: root
    password: root
    
rules:
  - !SHARDING
    tables:
      # 对d_order表进行分库分表
      d_order:
        # 库为damai_order_0 damai_order_1 表为d_order_0 至 d_order_3
        actualDataNodes: ds_${0..1}.d_order_${0..3}
        # 分库策略
        databaseStrategy:
          complex:
            # 使用order_number,user_id作为分片键
            shardingColumns: order_number,user_id
            # 使用order_number,user_id分库算法
            shardingAlgorithmName: databaseOrderComplexGeneArithmetic
        # 分表策略
        tableStrategy:
          complex:
            # 使用order_number,user_id作为分片键
            shardingColumns: order_number,user_id
            # 使用order_number,user_id分表算法
            shardingAlgorithmName: tableOrderComplexGeneArithmetic
      # 对d_order_ticket_user表进行分库分表
      d_order_ticket_user:
        # 库为damai_order_0 damai_order_1 表为d_order_ticket_user_0 至 d_order_ticket_user_3
        actualDataNodes: ds_${0..1}.d_order_ticket_user_${0..3}
        # 分库策略
        databaseStrategy:
          complex:
            # 使用order_number,user_id作为分片键
            shardingColumns: order_number,user_id
            # 使用order_number,user_id分库算法
            shardingAlgorithmName: databaseOrderTicketUserComplexGeneArithmetic
        # 分表策略
        tableStrategy:
          complex:
            # 使用order_number,user_id作为分片键
            shardingColumns: order_number,user_id
            # 使用order_number,user_id分表算法
            shardingAlgorithmName: tableOrderTicketUserComplexGeneArithmetic
    # 绑定表        
    bindingTables:
      - d_order,d_order_ticket_user
    # 具体的算法
    shardingAlgorithms:
      # d_order表分库算法
      databaseOrderComplexGeneArithmetic:
        # 通过自定义实现类实现分库算法
        type: CLASS_BASED
        props:
          # 分库数量
          sharding-count: 2
          # 分表数量
          table-sharding-count: 4
          # 分库策略,复合多分片
          strategy: complex
          # 具体的分库逻辑在此自定义类中
          algorithmClassName: com.damai.shardingsphere.DatabaseOrderComplexGeneArithmetic
      # d_order表分表算法
      tableOrderComplexGeneArithmetic:
        # 通过自定义实现类实现分表算法
        type: CLASS_BASED
        props:
          # 分表数量
          sharding-count: 4
          # 分表策略,复合多分片
          strategy: complex
          # 具体的分表逻辑在此自定义类中
          algorithmClassName: com.damai.shardingsphere.TableOrderComplexGeneArithmetic
      # d_order_ticket_user表分库算法
      databaseOrderTicketUserComplexGeneArithmetic:
        # 通过自定义实现类实现分库算法
        type: CLASS_BASED
        props:
          # 分库数量
          sharding-count: 2
          # 分表数量
          table-sharding-count: 4
          # 分库策略,复合多分片
          strategy: complex
          # 具体的分库逻辑在此自定义类中
          algorithmClassName: com.damai.shardingsphere.DatabaseOrderComplexGeneArithmetic
      # d_order_ticket_user表分表算法
      tableOrderTicketUserComplexGeneArithmetic:
        # 通过自定义实现类实现分表算法
        type: CLASS_BASED
        props:
          # 分表数量
          sharding-count: 4
          # 分表策略,复合多分片
          strategy: complex
          # 具体的分表逻辑在此自定义类中
          algorithmClassName: com.damai.shardingsphere.TableOrderComplexGeneArithmetic    
props:
  # 打印真实sql
  sql-show: true

总结

  • d_order表的分库分表都使用了order_number,user_id这两个字段一起作为分片键,并自定义了复合多分片类型的分库算法、分表算法
  • d_order_ticket_user表的分库分表都使用了order_number,user_id这两个字段一起作为分片键,并自定义了复合多分片类型的分库算法、分表算法
  • d_order和d_order_ticket_user指定了绑定表的关系
  • d_order和d_order_ticket_user的自定义分库算法的实现类是com.damai.shardingsphere.DatabaseOrderComplexGeneArithmetic
  • d_order和d_order_ticket_user的自定义分表算法的实现类是com.damai.shardingsphere.TableOrderComplexGeneArithmetic

在订单服务中,没有使用附属表路由的方式,而是使用了分片基因法来进行分库分表,无需额外的数据表路由,保证了执行的高效,建议小伙伴先学习分片基因法,再回来继续学习本文,跳转文档地址:

技术精华-解锁分库分表新姿势:基因法完全解读

当理解了基因法后,我们开始介绍订单服务的分片算法。为了便于理解,我们先从自定义分表算法开始介绍

自定义分表算法

public class TableOrderComplexGeneArithmetic implements ComplexKeysShardingAlgorithm<Long> {
    
    /**
     * 属性分表名
     * */
    private static final String SHARDING_COUNT_KEY_NAME = "sharding-count";
    /**
     * 分表数量
     * */
    private int shardingCount;
    
    @Override
    public void init(Properties props) {
        shardingCount = Integer.parseInt(props.getProperty(SHARDING_COUNT_KEY_NAME));
    }
    @Override
    public Collection<String> doSharding(Collection<String> allActualSplitTableNames, ComplexKeysShardingValue<Long> complexKeysShardingValue) {
        //返回的真实表名集合
        List<String> actualTableNames = new ArrayList<>(allActualSplitTableNames.size());
        //逻辑表名
        String logicTableName = complexKeysShardingValue.getLogicTableName();
        //查询中的列名和值
        Map<String, Collection<Long>> columnNameAndShardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
        //如果没有条件查询,那么就查所有的分表
        if (CollectionUtil.isEmpty(columnNameAndShardingValuesMap)) {
            return actualTableNames;
        }
        //order_number条件的值
        Collection<Long> orderNumberValues = columnNameAndShardingValuesMap.get("order_number");
        //user_id条件的值
        Collection<Long> userIdValues = columnNameAndShardingValuesMap.get("user_id");
        
        //分片键的值
        Long value = null;
        //如果是order_number查询
        if (CollectionUtil.isNotEmpty(orderNumberValues)) {
            value = orderNumberValues.stream().findFirst().orElseThrow(() -> new DaMaiFrameException(BaseCode.ORDER_NUMBER_NOT_EXIST));
          //如果是user_id查询
        } else if (CollectionUtil.isNotEmpty(userIdValues)) {
            value = userIdValues.stream().findFirst().orElseThrow(() -> new DaMaiFrameException(BaseCode.USER_ID_NOT_EXIST));
        }
        //如果order_number或者user_id的值存在
        if (Objects.nonNull(value)) {
            //逻辑表名_分片键的值对分表数量进行取模
            actualTableNames.add(logicTableName + "_" + ((shardingCount - 1) & value));
            return actualTableNames;
        }
        //如果没有分片键查询,则把所有真实表返回
        return allActualSplitTableNames;
    }
}

总结

  • 先获取到逻辑表名,也就是开发中sql的表名,比如d_order,用于后续真实表的构成
  • 从complexKeysShardingValue获取到columnNameAndShardingValuesMap,key为String 要查询或者操作数据的字段名,value为Collection 为具体的值
  • 分别从columnNameAndShardingValuesMap分别取order_number和user_id的值,哪个存在就用哪个值
  • 用取得值的对shardingCount分表数量进行取模,经过分片基因法替换后订单编号取模后得值,和使用用户id取模后的值是相同的,
  • 获得值就是定位到分片的索引位置
  • 将逻辑表和获取的索引值进行拼接成真实的表名
  • 将真实的表名返回
  • 如果操作中的条件没有order_number或者user_id的分片键,那么把所有的真实表返回,进行全路由读取

这里在对分表数量进行取模时是使用了HashMap中的思想,这样执行起来效率更高

actualTableNames.add(logicTableName + "_" + ((shardingCount - 1) & value));

shardingCount需要是2的n方

如果小伙伴不能理解的话,可以替换成正常的取模操作,获得的结果都是相同的

actualTableNames.add(logicTableName + "_" + value % shardingCount);

自定义分表算法介绍完后,接下来介绍分库算法

自定义分库算法

public class DatabaseOrderComplexGeneArithmetic implements ComplexKeysShardingAlgorithm<Long> {
    /**
     * 属性分库名
     * */
    private static final String SHARDING_COUNT_KEY_NAME = "sharding-count";
    
    /**
     * 属性分表名
     * */
    private static final String TABLE_SHARDING_COUNT_KEY_NAME = "table-sharding-count";
    
    /**
     * 分库数量
     * */
    private int shardingCount;
    
    /**
     * 分表数量
     * */
    private int tableShardingCount;
    
    @Override
    public void init(Properties props) {
        this.shardingCount = Integer.parseInt(props.getProperty(SHARDING_COUNT_KEY_NAME));
        this.tableShardingCount = Integer.parseInt(props.getProperty(TABLE_SHARDING_COUNT_KEY_NAME));
    }
    @Override
    public Collection<String> doSharding(Collection<String> allActualSplitDatabaseNames, ComplexKeysShardingValue<Long> complexKeysShardingValue) {
        //返回的真实库名集合
        List<String> actualDatabaseNames = new ArrayList<>(allActualSplitDatabaseNames.size());
        //查询中的列名和值
        Map<String, Collection<Long>> columnNameAndShardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
        //如果没有条件查询,那么就查所有的分表
        if (CollectionUtil.isEmpty(columnNameAndShardingValuesMap)) {
            return allActualSplitDatabaseNames;
        }
        //order_number条件的值
        Collection<Long> orderNumberValues = columnNameAndShardingValuesMap.get("order_number");
        //user_id条件的值
        Collection<Long> userIdValues = columnNameAndShardingValuesMap.get("user_id");
        
        Long value = null;
        //如果是order_number查询
        if (CollectionUtil.isNotEmpty(orderNumberValues)) {
            value = orderNumberValues.stream().findFirst().orElseThrow(() -> new DaMaiFrameException(BaseCode.ORDER_NUMBER_NOT_EXIST));
            //如果是user_id查询
        } else if (CollectionUtil.isNotEmpty(userIdValues)) {
            value = userIdValues.stream().findFirst().orElseThrow(() -> new DaMaiFrameException(BaseCode.USER_ID_NOT_EXIST));
        }
        //如果order_number或者user_id的值存在
        if (Objects.nonNull(value)) {
            //获得值后再获得实际的分库的索引
            long databaseIndex = calculateDatabaseIndex(shardingCount,value,tableShardingCount);
            String databaseIndexStr = String.valueOf(databaseIndex);
            for (String actualSplitDatabaseName : allActualSplitDatabaseNames) {
                //将所有的分库名和得到的分库索引进行匹配
                if (actualSplitDatabaseName.contains(databaseIndexStr)) {
                    actualDatabaseNames.add(actualSplitDatabaseName);
                    break;
                }
            }
            return actualDatabaseNames;
        }else {
            //如果没有分片键查询,则把所有真实库返回
            return allActualSplitDatabaseNames;
        }
    }
    
    /**
     * 计算给定表索引应分配到的数据库编号。
     *
     * @param databaseCount 数据库总数
     * @param splicingKey    分片键
     * @param tableCount    表总数
     * @return 分配到的数据库编号
     */
    public long calculateDatabaseIndex(Integer databaseCount, Long splicingKey, Integer tableCount) {
        // 将分片键换成二进制字符串
        String splicingKeyBinary = Long.toBinaryString(splicingKey);
        //找到被替换基因的长度
        long replacementLength = log2N(tableCount);
        //截取获得基因
        String geneBinaryStr = splicingKeyBinary.substring(splicingKeyBinary.length() - (int) replacementLength);
        
        if (StringUtil.isNotEmpty(geneBinaryStr)) {
            int h;
            //对基因的hashCode进行优化,使其分布更加均匀
            int geneOptimizeHashCode = (h = geneBinaryStr.hashCode()) ^ (h >>> 16);
            //进行取模
            return (databaseCount - 1) & geneOptimizeHashCode;
        }
        throw new DaMaiFrameException(BaseCode.NOT_FOUND_GENE);
    }
    
    public long log2N(long count) {
        return (long)(Math.log(count)/ Math.log(2));
    }
}

(h=geneBinaryStr.hashCode()) ^ (h >>> 16) Java HashMap 中的 哈希值扰动函数

的操作,将哈希值的高位与低位混合,减少哈希冲突的概率。

总结

  • 开始的步骤和自定义分表的算法逻辑是相同的,获取到分表中的索引值
  • 执行calculateDatabaseIndex方法通过分库数量,分片表的索引,分表数量来获得分片库的索引

下面来介绍calculateDatabaseIndex这个算法

  1. 首先将分片键(订单编号或者用户id)转换成二进制字符串
  2. 找到被替换基因的长度,这里计算tableCount分表数量的log2n对数
  3. 获取到长度后,根据长度开始截取二进制的分片键,获得被替换后的基因
  4. 对截取到的基因字符串的hashcode进行优化,使其分布更加均匀
  5. 对分库数量进行取模

这里的hashcode优化

int geneOptimizeHashCode = (h = geneBinaryStr.hashCode()) ^ (h >>> 16);

哈希值扰动函数(Hash Perturbation Function),通过 右移 16 位并异或 的操作,将哈希值的高位与低位混合,减少哈希冲突的概率。

以及对分库数量进行取模

(databaseCount - 1) & geneOptimizeHashCode;

是使用了HashMap中的源码,执行起来效率更高

除运算会消耗cpu的多个时钟周期,与运算cpu通常在一个时钟周期内就能完成。


企业级项目导航:⬅️ 03-分库分表-节目服务 | 04-分库分表-订单服务 | ➡️ 05-技术精华-全面剖析分库分表