--- title: "02-节目服务与支付服务分库分表设计思维全景" created: 2025-12-10 aliases: - 节目服务与支付服务分库分表设计思维全景 tags: - 项目 --- # 节目服务与支付服务分库分表设计思维全景 ## 视频 ![[image-b7f98b9e.png]] ## **一、节目服务(Program Service)设计分析** ### **1.1 需求分析:业务场景梳理** ![[需求分析:业务场景梳理-a4b5afbb.jpg]] ### **1.2 实体关系分析** ![[实体关系分析-e4e8faab.jpg]] ### **1.3 策略选择:为什么这样设计** ![[策略选择:为什么这样设计-fd618198.jpg]] ### **1.4 问题诊断:同频共振** ![[节目服务原设计问题-ecb2dd59.jpg]] ### **1.5 节目服务修正方案** ![[节目服务修正方案-627ec481.jpg]] ```java package com.damai.program.sharding; import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm; import java.util.Collection; import java.util.Properties; /** * 节目服务 - 层级分片算法 (Hierarchical Sharding) *

* 核心思想: * 将 2个库 * 2个表 视为 4个连续的全局槽位 (0, 1, 2, 3)。 * 通过 ID 取模计算出全局槽位,再通过“整除”确定库,“取余”确定表。 *

* 优势: * 相比于传统的 Hash(ID)%2 分库 + Hash(ID)%2 分表,此算法能避免 * "奇数ID落入库1表1,偶数ID落入库0表0",导致另外两张表为空的数据倾斜问题。 */ public class ProgramHierarchicalAlgorithm implements StandardShardingAlgorithm { private int dbCount; private int tableCount; // 算法类型标识:区分当前是用于计算分库(DATABASE)还是分表(TABLE) private String shardingType; @Override public void init(Properties props) { // 从配置文件加载参数,提供默认值防止报错 this.dbCount = Integer.parseInt(props.getProperty("db-count", "2")); this.tableCount = Integer.parseInt(props.getProperty("table-count", "2")); this.shardingType = props.getProperty("sharding-type", "DATABASE"); } @Override public String doSharding(Collection targets, PreciseShardingValue shardingValue) { // 1. 获取分片键的值 (节目ID) long id = shardingValue.getValue(); // 2. 计算全局槽位索引 (Global Slot Index) // 例如:2库2表共4个槽位。ID=5 -> 5%4=1 int totalSlots = dbCount * tableCount; // 使用 Math.abs 确保正数(虽然 Long ID 通常为正,但作为公共组件需健壮) int globalIndex = (int) (Math.abs(id) % totalSlots); // 3. 计算目标索引 (Target Index) int targetIndex; if ("DATABASE".equals(shardingType)) { // 分库逻辑:使用商 (Quotient) // 0/2=0, 1/2=0, 2/2=1, 3/2=1 -> 前两个槽位在库0,后两个在库1 targetIndex = globalIndex / tableCount; } else { // 分表逻辑:使用余数 (Remainder) // 0%2=0, 1%2=1, 2%2=0, 3%2=1 -> 表0, 表1, 表0, 表1 交替分布 targetIndex = globalIndex % tableCount; } // 4. 根据后缀匹配实际的数据源或表名 // targets 可能是 ["ds_0", "ds_1"] 或 ["program_0", "program_1"] String suffix = String.valueOf(targetIndex); return targets.stream() .filter(t -> t.endsWith("_" + suffix) || t.endsWith(suffix)) // 兼容 endsWith("0") 或 endsWith("_0") .findFirst() .orElseThrow(() -> new IllegalArgumentException("Target not found for index: " + targetIndex)); } @Override public String getType() { return "PROGRAM_HIERARCHICAL"; } } ``` ## **二、支付服务(Pay Service)设计分析** ### **2.1 需求分析:业务场景梳理** ![[支付服务需求分析-cbb6e9c7.jpg]] ### **2.2 实体关系分析** ![[支付服务实体关系-25c81871.jpg]] ### **2.3 策略分析与潜在问题** ![[支付服务策略分析-d149ba73.jpg]] ### **2.4 问题一:同频共振(与其他服务相同)** ![[支付服务同频共振问题-6c83eaf8.jpg]] ### **2.5 问题二:支付回调的全路由问题** ![[支付回调全路由问题分析-8ec4c49b.jpg]] ### **2.6 支付服务优化方案:基因法** ![[支付单号基因法设计-623af29a.jpg]] #### 支付单号生成器 (基因嵌入) 这是基因法的核心。在生成支付单号时,不随机生成,而是将订单号的 Hash 特征(基因)强制“烙印”在支付单号中。 ```java package com.damai.pay.util; import com.damai.common.util.SnowflakeIdGenerator; /** * 支付单号生成器 - 基因法 (Gene Method) *

* 作用: * 在生成支付单号 (pay_bill_no) 时,将外部订单号 (out_order_no) 的分片规则(基因)嵌入其中。 *

* 结果: * pay_bill_no 和 out_order_no 虽然数值不同,但它们对 TOTAL_SHARDS 取模的结果完全一致。 * 这保证了同一笔订单的支付记录,一定落在该订单所在的分片库/表中。 */ public class PayBillNoGenerator { // 分片总数 = 库数 * 表数 (此处假设 2库 * 2表 = 4) // 注意:基因法要求扩容时必须成倍扩容,且保持基因提取规则兼容 private static final int TOTAL_SHARDS = 4; /** * 生成带基因的支付单号 * * @param outOrderNo 外部订单号 (String 类型,因为订单号可能带字母或很长) * @return 嵌入了基因的支付单号 */ public static String generate(String outOrderNo) { // 1. 提取基因:计算订单号的分片基因 (0 ~ 3) // 必须与分片算法中的 hash 逻辑保持绝对一致 int gene = Math.abs(outOrderNo.hashCode()) % TOTAL_SHARDS; // 2. 生成全局唯一的序列号 (使用雪花算法或分布式序列号服务) long snowflakeId = SnowflakeIdGenerator.nextId(); // 3. 嵌入基因 // 策略:使用位移或数值乘法腾出位置,将基因放在最低位 // 公式:(ID * 分片总数) + 基因 // 举例:ID=100, Total=4, Gene=3 -> 100*4 + 3 = 403 // 403 % 4 = 3 (还原了基因) long payBillNo = (snowflakeId * TOTAL_SHARDS) + gene; return String.valueOf(payBillNo); } /** * 从支付单号中反向提取基因 * * @param payBillNo 支付单号 * @return 分片索引 (0 ~ TOTAL_SHARDS-1) */ public static int extractGene(String payBillNo) { try { long billNo = Long.parseLong(payBillNo); // 直接取模即可还原基因 return (int) (billNo % TOTAL_SHARDS); } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid pay bill no format", e); } } } ``` #### 支付服务分片算法 (双维度支持) 这个算法类非常巧妙,它同时处理 `out_order_no` (普通路由) 和 `pay_bill_no` (基因路由),实现了在同一个算法类中支持多种业务场景。 ```java package com.damai.pay.sharding; import com.damai.pay.util.PayBillNoGenerator; import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm; import java.util.Collection; import java.util.Properties; /** * 支付服务通用分片算法 *

* 支持双分片键路由: * 1. 按 out_order_no 路由:常规 Hash 取模。 * 2. 按 pay_bill_no 路由:提取嵌入的基因。 *

* 效果: * SELECT * FROM t_pay_bill WHERE out_order_no = 'O123'; -> 路由到 Shard X * SELECT * FROM t_pay_bill WHERE pay_bill_no = 'P456'; -> 也路由到 Shard X * (前提是 P456 是由 O123 生成的) */ public class PayBillShardingAlgorithm implements StandardShardingAlgorithm { private int dbCount; private int tableCount; private String shardingType; // DATABASE 或 TABLE @Override public void init(Properties props) { this.dbCount = Integer.parseInt(props.getProperty("db-count", "2")); this.tableCount = Integer.parseInt(props.getProperty("table-count", "2")); this.shardingType = props.getProperty("sharding-type", "DATABASE"); } @Override public String doSharding(Collection targets, PreciseShardingValue shardingValue) { // 获取当前 SQL 中的分片列名 String columnName = shardingValue.getColumnName(); String value = shardingValue.getValue(); int totalShards = dbCount * tableCount; // 核心逻辑:统一转换为全局基因 (Global Gene/Index) int gene; if ("out_order_no".equalsIgnoreCase(columnName)) { // 场景A:根据订单号查询 // 直接计算 Hash,逻辑必须与 PayBillNoGenerator 中的 gene 计算一致 gene = Math.abs(value.hashCode()) % totalShards; } else if ("pay_bill_no".equalsIgnoreCase(columnName)) { // 场景B:根据支付单号查询 // 提取单号中预埋的基因 gene = PayBillNoGenerator.extractGene(value); } else { throw new UnsupportedOperationException("Unsupported sharding column: " + columnName); } // 接下来使用层级分片逻辑,将基因映射到具体的库和表 int targetIndex; if ("DATABASE".equals(shardingType)) { // 基因 / 表数 = 库索引 targetIndex = gene / tableCount; } else { // 基因 % 表数 = 表索引 targetIndex = gene % tableCount; } // 匹配物理节点 String suffix = String.valueOf(targetIndex); return targets.stream() .filter(t -> t.endsWith("_" + suffix) || t.endsWith(suffix)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("Target not found for index: " + targetIndex)); } @Override public String getType() { return "PAY_BILL_SHARDING"; // 对应 YAML 中的 algorithmName } } ``` ## **三、修正后完整配置** ### **3.1 节目服务修正配置** ```yaml ====================================================================== shardingsphere-program.yaml Program 服务分片配置 策略:分层分片(Hierarchical Sharding) ====================================================================== dataSources: ds_0: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://127.0.0.1:3306/damai_program_0?... 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_program_1?... username: root password: root rules: - !SHARDING tables: # ========== 主 Program 表(d_program) ========== # 分片键:id(Long) # 算法:分层分片(先算全局槽位,再映射 DB / Table) d_program: actualDataNodes: ds_${0..1}.d_program_${0..1} databaseStrategy: standard: shardingColumn: id shardingAlgorithmName: programDbAlgorithm tableStrategy: standard: shardingColumn: id shardingAlgorithmName: programTableAlgorithm # ========== Program 分组表(d_program_group) ========== # 分片键:id(Long) d_program_group: actualDataNodes: ds_${0..1}.d_program_group_${0..1} databaseStrategy: standard: shardingColumn: id shardingAlgorithmName: programDbAlgorithm tableStrategy: standard: shardingColumn: id shardingAlgorithmName: programTableAlgorithm # ========== 演出场次表(d_program_show_time) ========== # 子表,按 program_id 分片,保证与主表数据同节点存储(Join 更高效) d_program_show_time: actualDataNodes: ds_${0..1}.d_program_show_time_${0..1} databaseStrategy: standard: shardingColumn: program_id shardingAlgorithmName: programDbAlgorithm tableStrategy: standard: shardingColumn: program_id shardingAlgorithmName: programTableAlgorithm # ========== 座位表(d_seat) ========== # 大数据量表,按 program_id 分片 d_seat: actualDataNodes: ds_${0..1}.d_seat_${0..1} databaseStrategy: standard: shardingColumn: program_id shardingAlgorithmName: programDbAlgorithm tableStrategy: standard: shardingColumn: program_id shardingAlgorithmName: programTableAlgorithm # ========== 票档表(d_ticket_category) ========== # 子表,按 program_id 分片。 d_ticket_category: actualDataNodes: ds_${0..1}.d_ticket_category_${0..1} databaseStrategy: standard: shardingColumn: program_id shardingAlgorithmName: programDbAlgorithm tableStrategy: standard: shardingColumn: program_id shardingAlgorithmName: programTableAlgorithm # ========== 广播表(Broadcast Tables) ========== # 小量、静态表,在所有库上完全复制,用于本地 Join broadcastTables: - d_program_category # ========== 分片算法定义 ========== shardingAlgorithms: # [自定义] Program 数据库分片算法 # 类:com.damai.program.sharding.ProgramHierarchicalAlgorithm # 逻辑:(id % 4) / 2 programDbAlgorithm: type: CLASS_BASED props: strategy: STANDARD algorithmClassName: com.damai.program.sharding.ProgramHierarchicalAlgorithm db-count: 2 table-count: 2 sharding-type: DATABASE # [自定义] Program 表分片算法 # 逻辑:(id % 4) % 2 programTableAlgorithm: type: CLASS_BASED props: strategy: STANDARD algorithmClassName: com.damai.program.sharding.ProgramHierarchicalAlgorithm db-count: 2 table-count: 2 sharding-type: TABLE props: sql-show: true ``` ### **3.2 支付服务修正配置** ```yaml ====================================================================== shardingsphere-pay.yaml Pay 服务分片配置 策略:分层分片 + 基因法(Gene Method) 目标: 1. 解决数据倾斜(分层分片) 2. 支付账单与订单数据同库同表,方便 Join ====================================================================== dataSources: ds_0: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://127.0.0.1:3306/damai_pay_0?... 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_pay_1?... username: root password: root rules: - !SHARDING tables: # ========== 支付账单表(d_pay_bill) ========== # 主分片键:out_order_no(关联订单 ID) # - 插入 / Join:使用 out_order_no 做 Hash # - 根据 pay_bill_no 查询:从 pay_bill_no 中提取 “基因” 再计算分片 d_pay_bill: actualDataNodes: ds_${0..1}.d_pay_bill_${0..1} databaseStrategy: standard: shardingColumn: out_order_no shardingAlgorithmName: payDbAlgorithm tableStrategy: standard: shardingColumn: out_order_no shardingAlgorithmName: payTableAlgorithm # ========== 退款表(d_refund_bill) ========== # 同样按 out_order_no 分片,确保退款账单与原支付单存放在同节点 d_refund_bill: actualDataNodes: ds_${0..1}.d_refund_bill_${0..1} databaseStrategy: standard: shardingColumn: out_order_no shardingAlgorithmName: payDbAlgorithm tableStrategy: standard: shardingColumn: out_order_no shardingAlgorithmName: payTableAlgorithm # ========== 分片算法定义 ========== shardingAlgorithms: # [自定义] Pay 数据库分片算法 # 类:com.damai.pay.sharding.PayBillShardingAlgorithm # 逻辑: # 1. 判断列名 # 2. out_order_no → Hash → 总槽位 → 分层计算 # 3. pay_bill_no → 提取 Gene → 分层计算 payDbAlgorithm: type: CLASS_BASED props: strategy: STANDARD algorithmClassName: com.damai.pay.sharding.PayBillShardingAlgorithm db-count: 2 table-count: 2 sharding-type: DATABASE # [自定义] Pay 表分片算法 # 逻辑同上,但返回表索引 payTableAlgorithm: type: CLASS_BASED props: strategy: STANDARD algorithmClassName: com.damai.pay.sharding.PayBillShardingAlgorithm db-count: 2 table-count: 2 sharding-type: TABLE props: sql-show: true ``` ## **四、设计总结对比** ![[三个服务分库分表对比-0bce9794.jpg]] ![[设计决策流程-2b9e03b8.jpg]] --- **企业级项目导航**:⬅️ [[01-用户服务分库分表设计思维全景|01-用户服务分库分表设计思维全景]] | 02-节目服务与支付服务分库分表设计思维全景 | ➡️ [[03-从决策到落地:如何根据业务优雅设计库表|03-从决策到落地:如何根据业务优雅设计库表]]