节目服务与支付服务分库分表设计思维全景
视频
一、节目服务(Program Service)设计分析
1.1 需求分析:业务场景梳理
1.2 实体关系分析
1.3 策略选择:为什么这样设计
1.4 问题诊断:同频共振
1.5 节目服务修正方案
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)
* <p>
* 核心思想:
* 将 2个库 * 2个表 视为 4个连续的全局槽位 (0, 1, 2, 3)。
* 通过 ID 取模计算出全局槽位,再通过“整除”确定库,“取余”确定表。
* <p>
* 优势:
* 相比于传统的 Hash(ID)%2 分库 + Hash(ID)%2 分表,此算法能避免
* "奇数ID落入库1表1,偶数ID落入库0表0",导致另外两张表为空的数据倾斜问题。
*/
public class ProgramHierarchicalAlgorithm implements StandardShardingAlgorithm<Long> {
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<String> targets, PreciseShardingValue<Long> 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 需求分析:业务场景梳理
2.2 实体关系分析
2.3 策略分析与潜在问题
2.4 问题一:同频共振(与其他服务相同)
2.5 问题二:支付回调的全路由问题
2.6 支付服务优化方案:基因法
支付单号生成器 (基因嵌入)
这是基因法的核心。在生成支付单号时,不随机生成,而是将订单号的 Hash 特征(基因)强制“烙印”在支付单号中。
package com.damai.pay.util;
import com.damai.common.util.SnowflakeIdGenerator;
/**
* 支付单号生成器 - 基因法 (Gene Method)
* <p>
* 作用:
* 在生成支付单号 (pay_bill_no) 时,将外部订单号 (out_order_no) 的分片规则(基因)嵌入其中。
* <p>
* 结果:
* 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 (基因路由),实现了在同一个算法类中支持多种业务场景。
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;
/**
* 支付服务通用分片算法
* <p>
* 支持双分片键路由:
* 1. 按 out_order_no 路由:常规 Hash 取模。
* 2. 按 pay_bill_no 路由:提取嵌入的基因。
* <p>
* 效果:
* 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<String> {
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<String> targets, PreciseShardingValue<String> 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 节目服务修正配置
======================================================================
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 支付服务修正配置
======================================================================
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
四、设计总结对比
企业级项目导航:⬅️ 01-用户服务分库分表设计思维全景 | 02-节目服务与支付服务分库分表设计思维全景 | ➡️ 03-从决策到落地:如何根据业务优雅设计库表
💬 评论