29 KiB
29 KiB
订单系统分库分表设计方案
业务场景分析
核心查询场景
订单表 (order)
- 订单列表:根据 userNo + 时间范围 + 订单类型 + 订单状态查询
- 订单详情:根据 orderNo 查询
凭证表 (ticket)
- 根据 orderNo 查询
- 根据 userNo + 状态查询
支付单表 (payment)
- 根据 orderNo 查询
- 根据 paymentNo 查询
- 根据 类型 + 状态查询
数据量预估
假设:
- 日订单量:100万
- 年订单量:3.6亿
- 3年数据:10亿+
分表策略
核心原则
- 按用户维度分表:大部分查询都带 userNo
- 保持关联关系:订单、凭证、支付单使用相同的分片键
- 支持多维度查询:通过冗余表或索引表解决
方案设计
方案一:按 userNo 分库分表(推荐)
分片规则
分片数:16 个库 × 16 张表 = 256 个分片
路由规则:
db_index = userNo % 16
table_index = (userNo / 16) % 16
示例:
userNo = 123456
db_index = 123456 % 16 = 0 → order_db_0
table_index = (123456 / 16) % 16 = 8 → order_8
表结构设计
1. 订单表 (order_0 ~ order_15)
CREATE TABLE `order_{0-15}` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`order_no` VARCHAR(64) NOT NULL COMMENT '订单编号(全局唯一)',
`user_no` VARCHAR(64) NOT NULL COMMENT '用户编号(分片键)',
`order_type` TINYINT NOT NULL COMMENT '订单类型:1-实时公交 2-定制巴士',
`order_status` TINYINT NOT NULL COMMENT '订单状态:1-待支付 2-已支付 3-已完成 4-已取消',
`route_id` VARCHAR(64) COMMENT '线路ID',
`schedule_id` VARCHAR(64) COMMENT '班次ID',
`passenger_count` INT COMMENT '乘客数量',
`total_amount` DECIMAL(10,2) COMMENT '订单金额',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`update_time` DATETIME NOT NULL COMMENT '更新时间',
`pay_time` DATETIME COMMENT '支付时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_create` (`user_no`, `create_time`),
KEY `idx_user_status` (`user_no`, `order_status`, `create_time`),
KEY `idx_user_type` (`user_no`, `order_type`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
索引说明:
uk_order_no:支持按订单号查询详情idx_user_create:支持用户订单列表(按时间排序)idx_user_status:支持按状态筛选idx_user_type:支持按类型筛选
2. 凭证表 (ticket_0 ~ ticket_15)
CREATE TABLE `ticket_{0-15}` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`ticket_no` VARCHAR(64) NOT NULL COMMENT '凭证编号(全局唯一)',
`order_no` VARCHAR(64) NOT NULL COMMENT '订单编号',
`user_no` VARCHAR(64) NOT NULL COMMENT '用户编号(分片键)',
`ticket_type` TINYINT NOT NULL COMMENT '凭证类型:1-单程票 2-往返票',
`ticket_status` TINYINT NOT NULL COMMENT '凭证状态:1-未使用 2-已使用 3-已过期 4-已作废',
`route_id` VARCHAR(64) COMMENT '线路ID',
`schedule_id` VARCHAR(64) COMMENT '班次ID',
`seat_no` VARCHAR(32) COMMENT '座位号',
`qr_code` VARCHAR(256) COMMENT '二维码',
`valid_start_time` DATETIME COMMENT '有效期开始',
`valid_end_time` DATETIME COMMENT '有效期结束',
`use_time` DATETIME COMMENT '使用时间',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`update_time` DATETIME NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ticket_no` (`ticket_no`),
KEY `idx_order_no` (`order_no`),
KEY `idx_user_status` (`user_no`, `ticket_status`, `create_time`),
KEY `idx_user_create` (`user_no`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='凭证表';
索引说明:
uk_ticket_no:支持按凭证号查询idx_order_no:支持按订单号查询凭证idx_user_status:支持用户凭证列表(按状态筛选)
3. 支付单表 (payment_0 ~ payment_15)
CREATE TABLE `payment_{0-15}` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`payment_no` VARCHAR(64) NOT NULL COMMENT '支付单编号(全局唯一)',
`order_no` VARCHAR(64) NOT NULL COMMENT '订单编号',
`user_no` VARCHAR(64) NOT NULL COMMENT '用户编号(分片键)',
`payment_type` TINYINT NOT NULL COMMENT '支付类型:1-支付 2-退款',
`payment_status` TINYINT NOT NULL COMMENT '支付状态:1-待支付 2-支付中 3-支付成功 4-支付失败',
`payment_channel` VARCHAR(32) COMMENT '支付渠道:ALIPAY/WECHAT/BALANCE',
`payment_amount` DECIMAL(10,2) NOT NULL COMMENT '支付金额',
`third_party_no` VARCHAR(128) COMMENT '第三方支付流水号',
`callback_time` DATETIME COMMENT '回调时间',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`update_time` DATETIME NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_payment_no` (`payment_no`),
KEY `idx_order_no` (`order_no`),
KEY `idx_user_create` (`user_no`, `create_time`),
KEY `idx_user_type_status` (`user_no`, `payment_type`, `payment_status`),
KEY `idx_third_party` (`third_party_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付单表';
索引说明:
uk_payment_no:支持按支付单号查询idx_order_no:支持按订单号查询支付单idx_user_type_status:支持按类型和状态筛选idx_third_party:支持第三方回调查询
方案二:订单号路由索引表(解决按 orderNo 查询)
问题
按 userNo 分表后,如果只有 orderNo,无法确定在哪个分片。
解决方案:订单号路由表
-- 单独的路由表(不分表或按 orderNo 分表)
CREATE TABLE `order_route` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(64) NOT NULL COMMENT '订单编号',
`user_no` VARCHAR(64) NOT NULL COMMENT '用户编号',
`db_index` TINYINT NOT NULL COMMENT '库索引',
`table_index` TINYINT NOT NULL COMMENT '表索引',
`create_time` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单路由表';
使用方式:
@Service
public class OrderQueryService {
/**
* 根据订单号查询订单详情
*/
public Order getOrderByOrderNo(String orderNo) {
// 1. 从路由表查询 userNo
OrderRoute route = orderRouteRepository.findByOrderNo(orderNo);
if (route == null) {
return null;
}
// 2. 根据 userNo 路由到具体分片查询
return orderRepository.findByOrderNo(route.getUserNo(), orderNo);
}
}
优化:路由表也可以分表
-- 按 orderNo 后4位分表(16张表)
CREATE TABLE `order_route_{0-15}` (
...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 路由规则
table_index = orderNo 后4位 % 16
方案三:业务单号中包含 userNo 信息(推荐)
统一编号生成规则
所有业务单号(订单号、凭证号、支付单号)都包含 userNo 信息,实现自路由。
1. 订单号生成规则
订单号格式:{业务前缀}{时间戳}{userNo后6位}{序列号}
示例:OD20240309123456789012001
解析:
- OD: 订单前缀
- 20240309: 日期
- 123456: 时间(时分秒)
- 789012: userNo 后6位(用于路由)
- 001: 序列号
2. 凭证号生成规则
凭证号格式:{业务前缀}{时间戳}{userNo后6位}{序列号}
示例:TK20240309123456789012001
解析:
- TK: 凭证前缀 (Ticket)
- 20240309: 日期
- 123456: 时间(时分秒)
- 789012: userNo 后6位(用于路由)
- 001: 序列号
3. 支付单号生成规则
支付单号格式:{业务前缀}{时间戳}{userNo后6位}{序列号}
示例:PY20240309123456789012001
解析:
- PY: 支付单前缀 (Payment)
- 20240309: 日期
- 123456: 时间(时分秒)
- 789012: userNo 后6位(用于路由)
- 001: 序列号
核心优点:
- 所有业务单号都支持自路由
- 无需额外的路由表
- 直接从单号解析出分片信息
- 性能最优
实现:
/**
* 统一业务单号生成器
*/
@Service
public class BusinessNoGenerator {
@Autowired
private RedisTemplate<String, Long> redisTemplate;
/**
* 生成订单号
*/
public String generateOrderNo(String userNo) {
return generateBusinessNo("OD", userNo);
}
/**
* 生成凭证号
*/
public String generateTicketNo(String userNo) {
return generateBusinessNo("TK", userNo);
}
/**
* 生成支付单号
*/
public String generatePaymentNo(String userNo) {
return generateBusinessNo("PY", userNo);
}
/**
* 统一生成业务单号
*/
private String generateBusinessNo(String prefix, String userNo) {
// 时间部分:yyyyMMddHHmmss
String timestamp = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
// userNo 后6位(用于路由)
String userSuffix = extractUserSuffix(userNo);
// 序列号(3位)- 使用 Redis 保证唯一性
String sequence = String.format("%03d", getSequence(prefix + timestamp));
return prefix + timestamp + userSuffix + sequence;
}
/**
* 提取 userNo 后6位
*/
private String extractUserSuffix(String userNo) {
if (userNo.length() <= 6) {
return String.format("%06d", Long.parseLong(userNo));
}
return userNo.substring(userNo.length() - 6);
}
/**
* 获取序列号(Redis 自增)
*/
private Long getSequence(String key) {
Long seq = redisTemplate.opsForValue().increment("seq:" + key);
return seq % 1000; // 3位序列号,循环使用
}
/**
* 从业务单号解析 userNo 后缀
*/
public String parseUserSuffix(String businessNo) {
// OD20240309123456789012001
// TK20240309123456789012001
// PY20240309123456789012001
// 从第16位开始,取6位
if (businessNo.length() >= 22) {
return businessNo.substring(16, 22);
}
return null;
}
/**
* 根据业务单号计算分片信息
*/
public ShardingInfo getShardingInfo(String businessNo) {
String userSuffix = parseUserSuffix(businessNo);
if (userSuffix == null) {
throw new IllegalArgumentException("Invalid businessNo: " + businessNo);
}
long userNoSuffix = Long.parseLong(userSuffix);
int dbIndex = (int) (userNoSuffix % 16);
int tableIndex = (int) ((userNoSuffix / 16) % 16);
return new ShardingInfo(dbIndex, tableIndex);
}
/**
* 判断业务单号类型
*/
public BusinessNoType getBusinessNoType(String businessNo) {
if (businessNo.startsWith("OD")) {
return BusinessNoType.ORDER;
} else if (businessNo.startsWith("TK")) {
return BusinessNoType.TICKET;
} else if (businessNo.startsWith("PY")) {
return BusinessNoType.PAYMENT;
}
throw new IllegalArgumentException("Unknown businessNo type: " + businessNo);
}
}
/**
* 业务单号类型枚举
*/
public enum BusinessNoType {
ORDER, // 订单
TICKET, // 凭证
PAYMENT // 支付单
}
/**
* 分片信息
*/
@Data
@AllArgsConstructor
public class ShardingInfo {
private int dbIndex;
private int tableIndex;
public String getDbName() {
return "ds" + dbIndex;
}
public String getTableSuffix() {
return String.valueOf(tableIndex);
}
}
查询实现
1. 订单列表查询(带 userNo)
@Service
public class OrderQueryService {
/**
* 查询用户订单列表
*/
public PageResult<Order> queryOrderList(OrderQueryDTO query) {
// 参数:userNo, startTime, endTime, orderType, orderStatus
// 1. 根据 userNo 路由到具体分片
ShardingInfo sharding = calculateSharding(query.getUserNo());
// 2. 构建查询条件
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("user_no", query.getUserNo());
if (query.getStartTime() != null) {
wrapper.ge("create_time", query.getStartTime());
}
if (query.getEndTime() != null) {
wrapper.le("create_time", query.getEndTime());
}
if (query.getOrderType() != null) {
wrapper.eq("order_type", query.getOrderType());
}
if (query.getOrderStatus() != null) {
wrapper.eq("order_status", query.getOrderStatus());
}
wrapper.orderByDesc("create_time");
// 3. 分页查询
Page<Order> page = new Page<>(query.getPageNo(), query.getPageSize());
Page<Order> result = orderMapper.selectPage(page, wrapper);
return PageResult.of(result);
}
}
SQL 示例:
-- 路由到 order_db_0.order_8
SELECT * FROM order_8
WHERE user_no = '123456'
AND create_time >= '2024-01-01'
AND create_time <= '2024-03-09'
AND order_type = 1
AND order_status = 2
ORDER BY create_time DESC
LIMIT 0, 20;
-- 使用索引:idx_user_type (user_no, order_type, create_time)
2. 订单详情查询(只有 orderNo)
@Service
public class OrderQueryService {
/**
* 根据订单号查询详情
*/
public Order getOrderDetail(String orderNo) {
// 方案一:从订单号解析分片信息(推荐)
ShardingInfo sharding = orderNoGenerator.getShardingInfo(orderNo);
// 方案二:查询路由表
// OrderRoute route = orderRouteRepository.findByOrderNo(orderNo);
// ShardingInfo sharding = new ShardingInfo(route.getDbIndex(), route.getTableIndex());
// 查询订单
return orderMapper.selectByOrderNo(sharding, orderNo);
}
}
SQL 示例:
-- 路由到 order_db_0.order_8
SELECT * FROM order_8
WHERE order_no = 'OD20240309123456789012001';
-- 使用索引:uk_order_no
3. 凭证查询
@Service
public class TicketQueryService {
@Autowired
private BusinessNoGenerator businessNoGenerator;
@Autowired
private TicketMapper ticketMapper;
/**
* 根据凭证号查询凭证详情(核心场景)
*/
public Ticket getTicketByTicketNo(String ticketNo) {
// 从凭证号直接解析分片信息
ShardingInfo sharding = businessNoGenerator.getShardingInfo(ticketNo);
// 查询凭证
return ticketMapper.selectByTicketNo(sharding, ticketNo);
}
/**
* 根据订单号查询凭证列表
*/
public List<Ticket> getTicketsByOrderNo(String orderNo) {
// 从订单号解析分片信息
ShardingInfo sharding = businessNoGenerator.getShardingInfo(orderNo);
// 查询凭证(凭证和订单在同一分片)
return ticketMapper.selectByOrderNo(sharding, orderNo);
}
/**
* 查询用户凭证列表
*/
public PageResult<Ticket> getUserTickets(TicketQueryDTO query) {
// 根据 userNo 路由
ShardingInfo sharding = calculateSharding(query.getUserNo());
QueryWrapper<Ticket> wrapper = new QueryWrapper<>();
wrapper.eq("user_no", query.getUserNo());
if (query.getStatus() != null) {
wrapper.eq("ticket_status", query.getStatus());
}
if (query.getStartTime() != null) {
wrapper.ge("create_time", query.getStartTime());
}
if (query.getEndTime() != null) {
wrapper.le("create_time", query.getEndTime());
}
wrapper.orderByDesc("create_time");
Page<Ticket> page = new Page<>(query.getPageNo(), query.getPageSize());
Page<Ticket> result = ticketMapper.selectPage(page, wrapper);
return PageResult.of(result);
}
/**
* 凭证核销(扫码场景)
*/
public Result<Boolean> verifyTicket(String ticketNo) {
// 1. 从凭证号解析分片
ShardingInfo sharding = businessNoGenerator.getShardingInfo(ticketNo);
// 2. 查询凭证
Ticket ticket = ticketMapper.selectByTicketNo(sharding, ticketNo);
if (ticket == null) {
return Result.fail("TICKET_NOT_FOUND", "凭证不存在");
}
if (ticket.getTicketStatus() != TicketStatus.UNUSED.getCode()) {
return Result.fail("TICKET_INVALID", "凭证状态异常");
}
// 3. 更新凭证状态
ticket.setTicketStatus(TicketStatus.USED.getCode());
ticket.setUseTime(LocalDateTime.now());
ticketMapper.updateById(ticket);
return Result.success(true);
}
}
SQL 示例:
-- 1. 根据凭证号查询(从凭证号解析到 ticket_db_5.ticket_12)
SELECT * FROM ticket_12
WHERE ticket_no = 'TK20240309123456789012001';
-- 使用索引:uk_ticket_no
-- 执行时间:<5ms
-- 2. 根据订单号查询凭证(从订单号解析到同一分片)
SELECT * FROM ticket_12
WHERE order_no = 'OD20240309123456789012001';
-- 使用索引:idx_order_no
-- 执行时间:<10ms
-- 3. 用户凭证列表
SELECT * FROM ticket_12
WHERE user_no = '123456'
AND ticket_status = 1
AND create_time >= '2024-01-01'
ORDER BY create_time DESC
LIMIT 0, 20;
-- 使用索引:idx_user_status
-- 执行时间:<30ms
4. 支付单查询
@Service
public class PaymentQueryService {
@Autowired
private BusinessNoGenerator businessNoGenerator;
@Autowired
private PaymentMapper paymentMapper;
@Autowired
private PaymentRouteRepository paymentRouteRepository;
/**
* 根据支付单号查询(核心场景)
*/
public Payment getPaymentByPaymentNo(String paymentNo) {
// 从支付单号直接解析分片信息
ShardingInfo sharding = businessNoGenerator.getShardingInfo(paymentNo);
return paymentMapper.selectByPaymentNo(sharding, paymentNo);
}
/**
* 根据订单号查询支付单列表
*/
public List<Payment> getPaymentsByOrderNo(String orderNo) {
// 从订单号解析分片信息
ShardingInfo sharding = businessNoGenerator.getShardingInfo(orderNo);
return paymentMapper.selectByOrderNo(sharding, orderNo);
}
/**
* 查询用户支付单列表
*/
public PageResult<Payment> getUserPayments(PaymentQueryDTO query) {
// 根据 userNo 路由
ShardingInfo sharding = calculateSharding(query.getUserNo());
QueryWrapper<Payment> wrapper = new QueryWrapper<>();
wrapper.eq("user_no", query.getUserNo());
if (query.getPaymentType() != null) {
wrapper.eq("payment_type", query.getPaymentType());
}
if (query.getPaymentStatus() != null) {
wrapper.eq("payment_status", query.getPaymentStatus());
}
if (query.getStartTime() != null) {
wrapper.ge("create_time", query.getStartTime());
}
if (query.getEndTime() != null) {
wrapper.le("create_time", query.getEndTime());
}
wrapper.orderByDesc("create_time");
Page<Payment> page = new Page<>(query.getPageNo(), query.getPageSize());
Page<Payment> result = paymentMapper.selectPage(page, wrapper);
return PageResult.of(result);
}
/**
* 第三方支付回调查询(特殊场景)
*
* 问题:第三方支付流水号不包含 userNo 信息,无法直接路由
* 解决方案:使用路由表
*/
public Payment getPaymentByThirdPartyNo(String thirdPartyNo) {
// 方案一:查询路由表(推荐)
PaymentRoute route = paymentRouteRepository.findByThirdPartyNo(thirdPartyNo);
if (route == null) {
return null;
}
ShardingInfo sharding = new ShardingInfo(route.getDbIndex(), route.getTableIndex());
return paymentMapper.selectByPaymentNo(sharding, route.getPaymentNo());
}
/**
* 创建支付单时,同步写入路由表
*/
@Transactional
public void createPayment(Payment payment) {
// 1. 写入支付单
paymentMapper.insert(payment);
// 2. 写入路由表(用于第三方回调查询)
if (StringUtils.isNotBlank(payment.getThirdPartyNo())) {
PaymentRoute route = new PaymentRoute();
route.setPaymentNo(payment.getPaymentNo());
route.setThirdPartyNo(payment.getThirdPartyNo());
route.setUserNo(payment.getUserNo());
ShardingInfo sharding = businessNoGenerator.getShardingInfo(payment.getPaymentNo());
route.setDbIndex(sharding.getDbIndex());
route.setTableIndex(sharding.getTableIndex());
route.setCreateTime(LocalDateTime.now());
paymentRouteRepository.save(route);
}
}
}
支付单路由表设计:
-- 支付单路由表(不分表,或按 third_party_no 分表)
CREATE TABLE `payment_route` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`payment_no` VARCHAR(64) NOT NULL COMMENT '支付单编号',
`third_party_no` VARCHAR(128) NOT NULL COMMENT '第三方支付流水号',
`user_no` VARCHAR(64) NOT NULL COMMENT '用户编号',
`db_index` TINYINT NOT NULL COMMENT '库索引',
`table_index` TINYINT NOT NULL COMMENT '表索引',
`create_time` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_third_party_no` (`third_party_no`),
KEY `idx_payment_no` (`payment_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付单路由表';
SQL 示例:
-- 1. 根据支付单号查询(从支付单号解析到 payment_db_3.payment_7)
SELECT * FROM payment_7
WHERE payment_no = 'PY20240309123456789012001';
-- 使用索引:uk_payment_no
-- 执行时间:<5ms
-- 2. 根据订单号查询支付单(从订单号解析到同一分片)
SELECT * FROM payment_7
WHERE order_no = 'OD20240309123456789012001';
-- 使用索引:idx_order_no
-- 执行时间:<10ms
-- 3. 第三方回调查询(先查路由表)
-- Step 1: 查询路由表
SELECT payment_no, db_index, table_index
FROM payment_route
WHERE third_party_no = '2024030912345678901234567890';
-- 执行时间:<5ms
-- Step 2: 根据路由信息查询支付单
SELECT * FROM payment_7
WHERE payment_no = 'PY20240309123456789012001';
-- 执行时间:<5ms
-- 总耗时:<10ms
-- 4. 用户支付单列表
SELECT * FROM payment_7
WHERE user_no = '123456'
AND payment_type = 1
AND payment_status = 3
AND create_time >= '2024-01-01'
ORDER BY create_time DESC
LIMIT 0, 20;
-- 使用索引:idx_user_type_status
-- 执行时间:<30ms
ShardingSphere 配置
1. 数据源配置
spring:
shardingsphere:
datasource:
names: ds0,ds1,ds2,ds3,ds4,ds5,ds6,ds7,ds8,ds9,ds10,ds11,ds12,ds13,ds14,ds15
# 配置16个数据源
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/order_db_0
username: root
password: password
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/order_db_1
username: root
password: password
# ... ds2 ~ ds15 配置类似
rules:
sharding:
# 分片算法
sharding-algorithms:
database-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_no.toLong() % 16}
table-inline:
type: INLINE
props:
algorithm-expression: order_$->{(user_no.toLong() / 16) % 16}
# 订单表分片规则
tables:
order:
actual-data-nodes: ds$->{0..15}.order_$->{0..15}
database-strategy:
standard:
sharding-column: user_no
sharding-algorithm-name: database-inline
table-strategy:
standard:
sharding-column: user_no
sharding-algorithm-name: table-inline
key-generate-strategy:
column: id
key-generator-name: snowflake
# 凭证表分片规则
ticket:
actual-data-nodes: ds$->{0..15}.ticket_$->{0..15}
database-strategy:
standard:
sharding-column: user_no
sharding-algorithm-name: database-inline
table-strategy:
standard:
sharding-column: user_no
sharding-algorithm-name: table-inline
# 支付单表分片规则
payment:
actual-data-nodes: ds$->{0..15}.payment_$->{0..15}
database-strategy:
standard:
sharding-column: user_no
sharding-algorithm-name: database-inline
table-strategy:
standard:
sharding-column: user_no
sharding-algorithm-name: table-inline
# 绑定表(关联查询优化)
binding-tables:
- order,ticket,payment
# 广播表(不分片的表)
broadcast-tables:
- order_route
props:
sql-show: true
2. 自定义分片算法(支持订单号路由)
@Component
public class OrderNoShardingAlgorithm implements StandardShardingAlgorithm<String> {
@Autowired
private OrderNoGenerator orderNoGenerator;
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<String> shardingValue) {
String columnValue = shardingValue.getValue();
// 判断是 userNo 还是 orderNo
if (columnValue.startsWith("OD")) {
// 订单号,解析出分片信息
ShardingInfo sharding = orderNoGenerator.getShardingInfo(columnValue);
if ("user_no".equals(shardingValue.getColumnName())) {
return "ds" + sharding.getDbIndex();
} else {
return "order_" + sharding.getTableIndex();
}
} else {
// userNo,直接计算
long userNo = Long.parseLong(columnValue);
int index = (int) (userNo % 16);
if ("user_no".equals(shardingValue.getColumnName())) {
return "ds" + index;
} else {
return "order_" + ((int) ((userNo / 16) % 16));
}
}
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<String> shardingValue) {
// 范围查询,返回所有分片
return availableTargetNames;
}
}
性能优化建议
1. 索引优化
-- 订单表核心索引
ALTER TABLE order_0 ADD INDEX idx_user_create (user_no, create_time);
ALTER TABLE order_0 ADD INDEX idx_user_status (user_no, order_status, create_time);
ALTER TABLE order_0 ADD INDEX idx_user_type (user_no, order_type, create_time);
-- 联合索引覆盖查询
ALTER TABLE order_0 ADD INDEX idx_user_list (
user_no, order_type, order_status, create_time, order_no, total_amount
);
2. 分区表(单表内再分区)
-- 按月分区
CREATE TABLE `order_0` (
...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (TO_DAYS(create_time)) (
PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
...
);
3. 冷热数据分离
-- 热数据表(近3个月)
CREATE TABLE order_hot_0 LIKE order_0;
-- 冷数据表(3个月以上)
CREATE TABLE order_cold_0 LIKE order_0;
-- 定时任务迁移冷数据
4. 读写分离
spring:
shardingsphere:
rules:
readwrite-splitting:
data-sources:
ds0:
write-data-source-name: ds0-master
read-data-source-names:
- ds0-slave1
- ds0-slave2
load-balancer-name: round-robin
总结
推荐方案
订单号包含 userNo 信息 + 按 userNo 分库分表
优点:
- 所有查询都能精确路由到单个分片
- 无需额外的路由表
- 性能最优
- 实现简单
分片规则:
- 16个库 × 16张表 = 256个分片
- 单分片数据量:10亿 / 256 ≈ 400万(可控)
查询性能:
- 订单列表(带 userNo):单分片查询,<50ms
- 订单详情(orderNo):单分片查询,<10ms
- 凭证查询:单分片查询,<20ms
- 支付单查询:单分片查询,<20ms
这个方案在大数据量场景下能保证高性能和可扩展性。