Skip to content

分库分表-订单服务

阅读此文前,建议小伙伴先阅读分库分表的前置知识,对分库分表 和 shardingsphere 有了大概的理解后,在继续阅读本文

技术精华-全面剖析分库分表

介绍

在开始介绍前,需要先知道大麦网中数据库表的关系是怎么样,关于数据库表设计的详细介绍,小伙伴可跳转到相关文档

基础讲解-数据库表关系

配置

引入 ShardingSphere 的相关依赖

xml
<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>

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

ShardingSphere 官网的规则配置说明:

数据分片 :: ShardingSphere

节目项目相关配置:

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

shardingsphere-order.yaml配置:

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
    hikari:
      # 连接池配置优化
      minimum-idle: 15                    # 最小空闲连接数(订单服务负载较高)
      maximum-pool-size: 80               # 最大连接池大小(订单服务负载较高)
      connection-timeout: 30000           # 连接超时时间(30秒)
      idle-timeout: 600000                # 空闲连接超时时间(10分钟)
      max-lifetime: 1800000               # 连接最大生命周期(30分钟)
      leak-detection-threshold: 60000     # 连接泄漏检测阈值(60秒)
  # 第二个订单库    
  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
    hikari:
      # 连接池配置优化
      minimum-idle: 15                    # 最小空闲连接数(订单服务负载较高)
      maximum-pool-size: 80               # 最大连接池大小(订单服务负载较高)
      connection-timeout: 30000           # 连接超时时间(30秒)
      idle-timeout: 600000                # 空闲连接超时时间(10分钟)
      max-lifetime: 1800000               # 连接最大生命周期(30分钟)
      leak-detection-threshold: 60000     # 连接泄漏检测阈值(60秒)

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
      # 对d_order_ticket_user_record表进行分库分表
      d_order_ticket_user_record:
        # 库为damai_order_0 damai_order_1 表为d_order_ticket_user_record_0 至 d_order_ticket_user_record_3
        actualDataNodes: ds_${0..1}.d_order_ticket_user_record_${0..3}
        # 分库策略
        databaseStrategy:
          complex:
            # 使用order_number,user_id作为分片键
            shardingColumns: order_number,user_id
            # 使用order_number,user_id分库算法
            shardingAlgorithmName: databaseOrderTicketUserRecordComplexGeneArithmetic
        tableStrategy:
          complex:
            # 使用order_number,user_id作为分片键
            shardingColumns: order_number,user_id
            # 使用order_number,user_id分表算法
            shardingAlgorithmName: tableOrderTicketUserRecordComplexGeneArithmetic
      # 对d_order_program表进行分库分表      
      d_order_program:
        # 库为damai_order_0 damai_order_1 表为d_order_program_0 至 d_order_program_1
        actualDataNodes: ds_${0..1}.d_order_program_${0..1}
        # 分库策略
        databaseStrategy:
          standard:
            # 使用program_id作为分片键
            shardingColumn: program_id
            # 使用program_id分库算法
            shardingAlgorithmName: databaseOrderProgramModModel
        tableStrategy:
          standard:
            # 使用program_id作为分片键
            shardingColumn: program_id
            # 使用program_id分表算法
            shardingAlgorithmName: tableOrderProgramModModel
    # 具体的算法
    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
      # d_order_ticket_user_record表分库算法 
      databaseOrderTicketUserRecordComplexGeneArithmetic:
        # 通过自定义实现类实现分库算法
        type: CLASS_BASED
        props:
          # 分库数量
          sharding-count: 2
          # 分表数量
          table-sharding-count: 4
          # 分库策略,复合多分片
          strategy: complex
          # 具体的分库逻辑在此自定义类中
          algorithmClassName: com.damai.shardingsphere.DatabaseOrderComplexGeneArithmetic
      # d_order_ticket_user_record表分表算法     
      tableOrderTicketUserRecordComplexGeneArithmetic:
        # 通过自定义实现类实现分表算法
        type: CLASS_BASED
        props:
          # 分表数量
          sharding-count: 4
          # 分表策略,复合多分片
          strategy: complex
          # 具体的分表逻辑在此自定义类中
          algorithmClassName: com.damai.shardingsphere.TableOrderComplexGeneArithmetic
      # d_order_program表分库算法 - 使用program_id的低位(bit0)
      databaseOrderProgramModModel:
        type: INLINE
        props:
          algorithm-expression: ds_${program_id % 2}
      # d_order_program表分表算法 - 使用program_id的次低位(bit1)
      tableOrderProgramModModel:
        type: INLINE
        props:
          algorithm-expression: d_order_program_${(program_id.intdiv(2)) % 2}
props:
  sql-show: true

总结

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

基因位分离算法

在分片算法中,使用了intdiv(2)的方式,这个在之前用户服务、节目服务、支付服务已经讲解了,这里就不再赘述了。

基因法

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

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

当理解了基因法后,我们开始介绍订单服务的分片算法。

核心思想

分库分表的核心就是:从订单号或userId的后几位中,取出不同的bit来决定去哪个库、哪张表。

plain
订单号/userId 后6位:[bit5][bit4][bit3][bit2][bit1][bit0]
                      └────┬────┘└────┬────┘
                        库基因      表基因
                      (高位bit)   (低位bit)

关键点:表基因用低位,库基因用中间位,两者不重叠!

位分配示例

2库4表 配置为例:

配置表基因位数库基因位数总共使用
2库4表2位 (bit0-1)1位 (bit2)3位
2库8表3位 (bit0-2)1位 (bit3)4位
4库8表3位 (bit0-2)2位 (bit3-4)5位
8库8表3位 (bit0-2)3位 (bit3-5)6位 (上限)

分表算法 - TableOrderComplexGeneArithmetic

核心代码

java
/**
 * 分表路由逻辑
 * 
 * 核心思路:
 * - 取分片键(订单号或userId)的低N位作为表索引
 * - N = log2(表数量),通过位运算 (shardingCount - 1) & value 实现
 * - 订单号和userId的低6位相同,所以两种查询都能路由到同一张表
 */
@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 或 user_id)
    Collection<Long> orderNumberValues = columnNameAndShardingValuesMap.get("order_number");
    Collection<Long> userIdValues = columnNameAndShardingValuesMap.get("user_id");
    
    Long value = null;
    if (CollectionUtil.isNotEmpty(orderNumberValues)) {
        value = orderNumberValues.stream().findFirst()
                .orElseThrow(() -> new DaMaiFrameException(BaseCode.ORDER_NUMBER_NOT_EXIST));
    } else if (CollectionUtil.isNotEmpty(userIdValues)) {
        value = userIdValues.stream().findFirst()
                .orElseThrow(() -> new DaMaiFrameException(BaseCode.USER_ID_NOT_EXIST));
    }
    
    if (Objects.nonNull(value)) {
        // 核心算法:表索引 = 分片键的低N位
        // (shardingCount - 1) & value
        actualTableNames.add(logicTableName + "_" + ((shardingCount - 1) & value));
        return actualTableNames;
    }
    
    return allActualSplitTableNames;
}

算法图解

plain
分表算法核心公式:tableIndex = (tableCount - 1) & shardingKey

以 4表 为例(tableCount = 4):
  tableCount - 1 = 3 = 0b11(掩码)
  
  shardingKey = 37 = 0b100101
                          └┬┘
                       取后2位 = 01 = 1
  
  结果:路由到 d_order_1

执行流程

plain
┌─────────────────────────────────────────────────────────────┐
│                        SQL 查询                              │
│  SELECT * FROM d_order WHERE order_number = 5178139721619749│
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 第1步:提取分片键                                            │
│ shardingKey = 5178139721619749                               │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 第2步:计算表索引                                            │
│ tableIndex = (4 - 1) & shardingKey                          │
│            = 3 & 5178139721619749                            │
│            = 0b11 & 0b...100101                              │
│            = 0b01 = 1                                        │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 第3步:拼接物理表名                                          │
│ 物理表名 = "d_order" + "_" + 1 = "d_order_1"                 │
└─────────────────────────────────────────────────────────────┘

分库算法 - DatabaseOrderComplexGeneArithmetic

核心代码

java
/**
 * 计算分库索引
 * 
 * 核心思路:
 * - 分表使用ID的低位bit(最后 log2(tableCount) 位)
 * - 分库使用ID的中高位bit(跳过表基因位后的 log2(databaseCount) 位)
 * - 直接使用位运算,避免hashCode导致的分布不均
 *
 * @param databaseCount 数据库总数
 * @param splicingKey   分片键(订单号或userId,低6位包含相同基因)
 * @param tableCount    表总数
 * @return 分配到的数据库编号
 */
public long calculateDatabaseIndex(Integer databaseCount, Long splicingKey, Integer tableCount) {
    // 计算表分片占用的bit位数
    long tableGeneLength = log2N(tableCount);
    
    // 将分片键右移tableGeneLength位,跳过表基因位
    // 然后与(databaseCount-1)进行按位与运算,得到库索引
    return (databaseCount - 1) & (splicingKey >> tableGeneLength);
}

public long log2N(long count) {
    return (long)(Math.log(count)/ Math.log(2));
}

算法图解

plain
分库算法核心公式:dbIndex = (dbCount - 1) & (shardingKey >> tableGeneLength)

以 2库4表 为例:
  tableGeneLength = log2(4) = 2
  dbCount - 1 = 1 = 0b1(掩码)
  
  shardingKey = 37 = 0b100101
                        │ └┬┘
                        │  表基因(bit0-1)
                        └── 库基因(bit2)
  
  Step1: 右移2位,跳过表基因
         37 >> 2 = 0b1001 = 9
  
  Step2: 取低1位
         9 & 1 = 1
  
  结果:路由到 ds_1

执行流程

plain
┌─────────────────────────────────────────────────────────────┐
│                        SQL 查询                              │
│  SELECT * FROM d_order WHERE order_number = 5178139721619749│
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 第1步:提取分片键                                            │
│ shardingKey = 5178139721619749                               │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 第2步:计算表基因位数                                        │
│ tableGeneLength = log2(4) = 2                                │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 第3步:右移跳过表基因                                        │
│ shardingKey >> 2 = 5178139721619749 >> 2                     │
│                  = 1294534930404937                          │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 第4步:计算库索引                                            │
│ dbIndex = (2 - 1) & 1294534930404937                         │
│         = 1 & 1294534930404937                               │
│         = 1                                                  │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 第5步:匹配物理库名                                          │
│ 物理库名 = "ds_1"                                            │
└─────────────────────────────────────────────────────────────┘

真实数据示例

场景:userId = 37 的订单路由

配置:2库4表

plain
userId = 37
二进制 = 100101
后6位 = 100101

分解:
  [bit5][bit4][bit3][bit2][bit1][bit0]
    1     0     0     1     0     1
                      │     └──┬──┘
                      │      表基因 = 01 = 1
                      └── 库基因 = 1

计算分表:

plain
tableIndex = (4 - 1) & 37
           = 3 & 37
           = 0b11 & 0b100101
           = 0b01 = 1
           
结果:d_order_1

计算分库:

plain
tableGeneLength = log2(4) = 2
dbIndex = (2 - 1) & (37 >> 2)
        = 1 & 9
        = 0b1 & 0b1001
        = 1
        
结果:ds_1

最终路由:ds_1.d_order_1

userId查询与订单号查询的一致性

plain
userId = 37
  后6位 = 100101
  
orderNumber = 5178139721619749(由userId=37生成)
  后6位 = 100101(与userId相同!)

┌─────────────────────────────────────────────────────────────┐
│                  用 userId 查询                              │
│  SELECT * FROM d_order WHERE user_id = 37                   │
│                                                              │
│  分表:(4-1) & 37 = 1 → d_order_1                           │
│  分库:(2-1) & (37>>2) = 1 → ds_1                           │
│  路由:ds_1.d_order_1                                        │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                用 orderNumber 查询                           │
│  SELECT * FROM d_order WHERE order_number = 5178139721619749│
│                                                              │
│  分表:(4-1) & 5178139721619749 = 1 → d_order_1             │
│  分库:(2-1) & (5178139721619749>>2) = 1 → ds_1             │
│  路由:ds_1.d_order_1                                        │
└─────────────────────────────────────────────────────────────┘

两种查询路由到同一位置!这就是基因法的核心价值!

为什么没有奇偶隔离问题?

有人可能担心:会不会奇数库只有奇数表,偶数库只有偶数表?

答案是不会!因为库和表取的是不同的bit位。

plain
以 userId 0-7 为例(2库4表配置):

userId  二进制   bit2(库)  bit0-1(表)  路由结果
─────────────────────────────────────────────
  0      000       0          0      ds_0.d_order_0
  1      001       0          1      ds_0.d_order_1
  2      010       0          2      ds_0.d_order_2
  3      011       0          3      ds_0.d_order_3
  4      100       1          0      ds_1.d_order_0
  5      101       1          1      ds_1.d_order_1
  6      110       1          2      ds_1.d_order_2
  7      111       1          3      ds_1.d_order_3

ds_0 包含:d_order_0, d_order_1, d_order_2, d_order_3
ds_1 包含:d_order_0, d_order_1, d_order_2, d_order_3

每个库都完整包含所有分表!

配置驱动特性

分片算法通过yaml配置获取参数,扩容时只需改配置:

yaml
# 当前配置:2库4表
shardingAlgorithms:
  database-order-complex-gene-model:
    type: CLASS_BASED
    props:
      strategy: COMPLEX
      algorithmClassName: com.damai.shardingsphere.DatabaseOrderComplexGeneArithmetic
      sharding-count: 2        # 2个库
      table-sharding-count: 4  # 4张表
      
  table-order-complex-gene-model:
    type: CLASS_BASED
    props:
      strategy: COMPLEX
      algorithmClassName: com.damai.shardingsphere.TableOrderComplexGeneArithmetic
      sharding-count: 4        # 4张表

扩容到2库8表,只需改配置:

yaml
# 扩容后配置:2库8表
shardingAlgorithms:
  database-order-complex-gene-model:
    props:
      sharding-count: 2        # 还是2个库
      table-sharding-count: 8  # 改成8张表
      
  table-order-complex-gene-model:
    props:
      sharding-count: 8        # 改成8张表

代码完全不用动!

更新: 2026-02-06 18:03:25
原文: https://www.yuque.com/u22210564/ykdrdh/qp45tvdwuqmebe75

Java 后端面试知识库