--- title: "04-分库分表-订单服务" created: 2025-12-10 aliases: - 分库分表-订单服务 tags: - 项目 --- # 分库分表-订单服务 ## 配置 引入 ShardingSphere 的相关依赖 ```xml 5.3.2 org.apache.shardingsphere shardingsphere-jdbc-core ${shardingsphere.version} logback-classic ch.qos.logback ``` ## 根据规则进行分库分表的规则配置 节目项目相关配置: ```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 # 第二个订单库 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 在订单服务中,没有使用附属表路由的方式,而是使用了分片基因法来进行分库分表,无需额外的数据表路由,保证了执行的高效,建议小伙伴先学习分片基因法,再回来继续学习本文,跳转文档地址: [[07-技术精华-解锁分库分表新姿势:基因法完全解读|技术精华-解锁分库分表新姿势:基因法完全解读]] 当理解了基因法后,我们开始介绍订单服务的分片算法。为了便于理解,我们先从自定义分表算法开始介绍 ## 自定义分表算法 ```java public class TableOrderComplexGeneArithmetic implements ComplexKeysShardingAlgorithm { /** * 属性分表名 * */ 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 doSharding(Collection allActualSplitTableNames, ComplexKeysShardingValue complexKeysShardingValue) { //返回的真实表名集合 List actualTableNames = new ArrayList<>(allActualSplitTableNames.size()); //逻辑表名 String logicTableName = complexKeysShardingValue.getLogicTableName(); //查询中的列名和值 Map> columnNameAndShardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap(); //如果没有条件查询,那么就查所有的分表 if (CollectionUtil.isEmpty(columnNameAndShardingValuesMap)) { return actualTableNames; } //order_number条件的值 Collection orderNumberValues = columnNameAndShardingValuesMap.get("order_number"); //user_id条件的值 Collection 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中的思想,这样执行起来效率更高 ```java actualTableNames.add(logicTableName + "_" + ((shardingCount - 1) & value)); ``` shardingCount需要是2的n方 如果小伙伴不能理解的话,可以替换成正常的取模操作,获得的结果都是相同的 ```java actualTableNames.add(logicTableName + "_" + value % shardingCount); ``` 自定义分表算法介绍完后,接下来介绍分库算法 ## 自定义分库算法 ```java public class DatabaseOrderComplexGeneArithmetic implements ComplexKeysShardingAlgorithm { /** * 属性分库名 * */ 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 doSharding(Collection allActualSplitDatabaseNames, ComplexKeysShardingValue complexKeysShardingValue) { //返回的真实库名集合 List actualDatabaseNames = new ArrayList<>(allActualSplitDatabaseNames.size()); //查询中的列名和值 Map> columnNameAndShardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap(); //如果没有条件查询,那么就查所有的分表 if (CollectionUtil.isEmpty(columnNameAndShardingValuesMap)) { return allActualSplitDatabaseNames; } //order_number条件的值 Collection orderNumberValues = columnNameAndShardingValuesMap.get("order_number"); //user_id条件的值 Collection 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优化 ```java int geneOptimizeHashCode = (h = geneBinaryStr.hashCode()) ^ (h >>> 16); ``` 哈希值扰动函数(Hash Perturbation Function),通过 右移 16 位并异或 的操作,将哈希值的高位与低位混合,减少哈希冲突的概率。 以及对分库数量进行取模 ```text (databaseCount - 1) & geneOptimizeHashCode; ``` 是使用了HashMap中的源码,执行起来效率更高 除运算会消耗cpu的多个时钟周期,与运算cpu通常在一个时钟周期内就能完成。 --- **企业级项目导航**:⬅️ [[03-分库分表-节目服务|03-分库分表-节目服务]] | 04-分库分表-订单服务 | ➡️ [[05-技术精华-全面剖析分库分表|05-技术精华-全面剖析分库分表]]