分库分表-订单服务
阅读此文前,建议小伙伴先阅读分库分表的前置知识,对分库分表 和 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 官网的规则配置说明:
节目项目相关配置:
yaml
spring:
datasource:
driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
url: jdbc:shardingsphere:classpath:shardingsphere-order.yamlshardingsphere-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_order和d_order_ticket_user的自定义分库算法的实现类是com.damai.shardingsphere.DatabaseOrderComplexGeneArithmeticd_order和d_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