注册

订单表超10亿数据,如何设计Sharding策略?解决跨分片查询和分布式事务?

订单表超10亿数据,如何设计Sharding策略?解决跨分片查询和分布式事务?



引言:

在电商平台高速发展的今天,海量订单处理已成为技术团队必须面对的挑战。当订单数据突破10亿大关,传统单库架构在查询性能、存储容量和运维复杂度上都会遇到瓶颈。


作为有8年经验的Java工程师,我曾主导多个日订单量百万级系统的分库分表改造。今天我将分享从Sharding策略设计到分布式事务落地的完整解决方案,其中包含核心代码实现和实战避坑指南。



一、业务场景分析


1.1 订单数据特点



  • 数据量大:日增订单50万+,年增1.8亿
  • 访问模式

    • 写操作:高频下单(峰值5000 TPS)
    • 读操作:订单查询(用户端+运营端)


  • 数据生命周期:热数据(3个月)占80%访问量

1.2 核心挑战


graph LR
A[10亿级订单] --> B[查询性能]
A --> C[存储瓶颈]
A --> D[跨分片聚合]
A --> E[分布式事务]

二、Sharding策略设计


2.1 分片键选择


候选方案优点缺点适用场景
用户ID用户维度查询快可能导致数据倾斜C端主导业务
订单ID数据分布均匀用户订单需跨分片查询均匀分布场景
商户ID商户维度查询快C端查询效率低B2B平台
创建时间冷热数据分离范围查询可能跨分片推荐方案

最终方案:复合分片键(用户ID+创建时间)


2.2 分片算法设计


/**
* 自定义复合分片算法
* 分片键:user_id + create_time
*/

public class OrderShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {

private static final String USER_KEY = "user_id";
private static final String TIME_KEY = "create_time";

@Override
public Collection<String> doSharding(
Collection<String> availableTargetNames,
ComplexKeysShardingValue<Long> shardingValue)
{

Map<String, Collection<Long>> columnMap = shardingValue.getColumnNameAndShardingValuesMap();
List<String> shardingResults = new ArrayList<>();

// 获取用户ID分片值
Collection<Long> userIds = columnMap.get(USER_KEY);
Long userId = userIds.stream().findFirst().orElseThrow();

// 获取时间分片值
Collection<Long> timestamps = columnMap.get(TIME_KEY);
Long createTime = timestamps.stream().findFirst().orElse(System.currentTimeMillis());

// 计算用户分片(16个分库)
int dbShard = Math.abs(userId.hashCode()) % 16;

// 计算时间分片(按月分表)
LocalDateTime dateTime = Instant.ofEpochMilli(createTime)
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
String tableSuffix = dateTime.format(DateTimeFormatter.ofPattern("yyyyMM"));

// 构建目标分片
String targetDB = "order_db_" + dbShard;
String targetTable = "t_order_" + tableSuffix;
shardingResults.add(targetDB + "." + targetTable);

return shardingResults;
}
}

2.3 分片策略配置(ShardingSphere)


# application-sharding.yaml
spring:
shardingsphere:
datasource:
names: ds0,ds1,...,ds15
# 配置16个数据源...

sharding:
tables:
t_order:
actualDataNodes: ds${0..15}.t_order_${202301..202412}
tableStrategy:
complex:
shardingColumns: user_id,create_time
algorithmClassName: com.xxx.OrderShardingAlgorithm
keyGenerator:
column: order_id
type: SNOWFLAKE

三、跨分片查询解决方案


3.1 常见问题及对策


问题类型传统方案痛点优化方案
分页查询LIMIT 0,10 扫描全表二次查询法
排序聚合内存合并性能差并行查询+流式处理
全局索引无法直接建立异步构建ES索引

3.2 分页查询优化实现


/**
* 跨分片分页查询优化(二次查询法)
* 原SQL:SELECT * FROM t_order WHERE user_id=1001 ORDER BY create_time DESC LIMIT 10000,10
*/

public Page<Order> shardingPageQuery(Long userId, int pageNo, int pageSize) {

// 第一步:全分片并行查询
List<Order> allShardResults = shardingExecute(
shard -> "SELECT order_id, create_time FROM t_order "
+ "WHERE user_id = " + userId
+ " ORDER BY create_time DESC"
);

// 第二步:内存排序取TopN
List<Long> targetIds = allShardResults.stream()
.sorted(Comparator.comparing(Order::getCreateTime).reversed())
.skip(pageNo * pageSize)
.limit(pageSize)
.map(Order::getOrderId)
.collect(Collectors.toList());

// 第三步:精准查询目标数据
return orderRepository.findByIdIn(targetIds);
}

/**
* 并行执行查询(使用CompletableFuture)
*/

private List<Order> shardingExecute(Function<Integer, String> sqlBuilder) {
List<CompletableFuture<List<Order>>> futures = new ArrayList<>();
for (int i = 0; i < 16; i++) {
final int shardId = i;
futures.add(CompletableFuture.supplyAsync(() -> {
String sql = sqlBuilder.apply(shardId);
return jdbcTemplate.query(sql, new OrderRowMapper());
}, shardingThreadPool));
}
return futures.stream()
.map(CompletableFuture::join)
.flatMap(List::stream)
.collect(Collectors.toList());
}

3.3 聚合查询优化


/**
* 分布式聚合计算(如:用户总订单金额)
* 方案:并行查询分片结果 + 内存汇总
*/

public BigDecimal calculateUserTotalAmount(Long userId) {
List<CompletableFuture<BigDecimal>> futures = new ArrayList<>();

for (int i = 0; i < 16; i++) {
futures.add(CompletableFuture.supplyAsync(() -> {
String sql = "SELECT SUM(amount) FROM t_order WHERE user_id = ?";
return jdbcTemplate.queryForObject(
sql, BigDecimal.class, userId);
}, shardingThreadPool));
}

return futures.stream()
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}

四、分布式事务解决方案


4.1 方案对比


方案一致性性能复杂度适用场景
2PC强一致银行核心系统
TCC强一致资金交易
Saga最终一致订单系统(推荐)
本地消息表最终一致低要求场景

4.2 Saga事务实现(订单创建场景)


sequenceDiagram
participant C as 应用
participant O as 订单服务
participant I as 库存服务
participant P as 支付服务

C->>O: 创建订单
O->>I: 预扣库存
I-->>O: 扣减成功
O->>P: 发起支付
P-->>O: 支付成功
O->>C: 返回结果

alt 支付失败
O->>I: 释放库存(补偿)
end

4.3 核心代码实现


/**
* Saga事务管理器(使用Seata框架)
*/

@Service
@Slf4j
public class OrderSagaService {

@Autowired
private InventoryFeignClient inventoryClient;

@Autowired
private PaymentFeignClient paymentClient;

@Transactional
public void createOrder(OrderCreateDTO dto) {
// 1. 创建本地订单(状态:待支付)
Order order = createPendingOrder(dto);

try {
// 2. 调用库存服务(Saga参与者)
inventoryClient.deductStock(
new DeductRequest(order.getOrderId(), dto.getSkuItems()));

// 3. 调用支付服务(Saga参与者)
paymentClient.createPayment(
new PaymentRequest(order.getOrderId(), order.getTotalAmount()));

// 4. 更新订单状态为已支付
order.paySuccess();
orderRepository.update(order);

} catch (Exception ex) {
// 触发Saga补偿流程
log.error("订单创建失败,触发补偿", ex);
handleCreateOrderFailure(order, ex);
throw ex;
}
}

/**
* 补偿操作(需要幂等)
*/

@Compensable(compensationMethod = "compensateOrder")
private void handleCreateOrderFailure(Order order, Exception ex) {
// 1. 释放库存
inventoryClient.restoreStock(order.getOrderId());

// 2. 取消支付(如果已发起)
paymentClient.cancelPayment(order.getOrderId());

// 3. 标记订单失败
order.cancel("系统异常: " + ex.getMessage());
orderRepository.update(order);
}

/**
* 补偿方法(幂等设计)
*/

public void compensateOrder(Order order, Exception ex) {
// 通过状态判断避免重复补偿
if (order.getStatus() != OrderStatus.CANCELLED) {
handleCreateOrderFailure(order, ex);
}
}
}

五、性能优化实践


5.1 分片路由优化


/**
* 热点用户订单查询优化
* 方案:用户分片路由缓存
*/

@Aspect
@Component
public class ShardingRouteCacheAspect {

@Autowired
private RedisTemplate<String, Integer> redisTemplate;

private static final String ROUTE_KEY = "user_route:%d";

@Around("@annotation(org.apache.shardingsphere.api.hint.Hint)")
public Object cacheRoute(ProceedingJoinPoint joinPoint) throws Throwable {
Long userId = getUserIdFromArgs(joinPoint.getArgs());
if (userId == null) {
return joinPoint.proceed();
}

// 1. 查询缓存
String cacheKey = String.format(ROUTE_KEY, userId);
Integer shardId = redisTemplate.opsForValue().get(cacheKey);

if (shardId == null) {
// 2. 计算分片ID(避免全表扫描)
shardId = calculateUserShard(userId);
redisTemplate.opsForValue().set(cacheKey, shardId, 1, TimeUnit.HOURS);
}

// 3. 设置分片Hint强制路由
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.setDatabaseShardingValue(shardId);
return joinPoint.proceed();
}
}

private int calculateUserShard(Long userId) {
// 分片计算逻辑(与分片算法保持一致)
return Math.abs(userId.hashCode()) % 16;
}
}

5.2 冷热数据分离


-- 归档策略示例(每月执行)
CREATE EVENT archive_orders
ON SCHEDULE EVERY 1 MONTH
DO
BEGIN
-- 1. 创建归档表(按年月)
SET @archive_table = CONCAT('t_order_archive_', DATE_FORMAT(NOW(), '%Y%m'));
SET @create_sql = CONCAT('CREATE TABLE IF NOT EXISTS ', @archive_table, ' LIKE t_order');
PREPARE stmt FROM @create_sql; EXECUTE stmt;

-- 2. 迁移数据(6个月前)
SET @move_sql = CONCAT(
'INSERT INTO ', @archive_table,
' SELECT * FROM t_order WHERE create_time < DATE_SUB(NOW(), INTERVAL 6 MONTH)'
);
PREPARE stmt FROM @move_sql; EXECUTE stmt;

-- 3. 删除原表数据
DELETE FROM t_order WHERE create_time < DATE_SUB(NOW(), INTERVAL 6 MONTH);
END

六、避坑指南


6.1 常见问题及解决方案


问题类型现象解决方案
分片键选择不当数据倾斜(70%数据在1个分片)增加分片基数(复合分片键)
分布式事务超时库存释放失败增加重试机制+人工补偿台
跨分片查询性能差分页查询超时改用ES做全局搜索
扩容困难增加分片需迁移数据初始设计预留分片(32库)

6.2 必须实现的监控项


graph TD
A[监控大盘] --> B[分片负载]
A --> C[慢查询TOP10]
A --> D[分布式事务成功率]
A --> E[热点用户检测]
A --> F[归档任务状态]

七、总结与展望


分库分表本质是业务与技术的平衡艺术,经过多个项目的实践验证,我总结了以下核心经验:



  1. 分片设计三原则

    • 数据分布均匀性 > 查询便捷性
    • 业务可扩展性 > 短期性能
    • 简单可运维 > 技术先进性


  2. 演进路线建议
    graph LR
    A[单库] --> B[读写分离]
    B --> C[垂直分库]
    C --> D[水平分表]
    D --> E[单元化部署]


  3. 未来优化方向

    • 基于TiDB的HTAP架构
    • 使用Apache ShardingSphere-Proxy
    • 智能分片路由(AI预测热点)




最后的话:

处理10亿级订单如同指挥一场交响乐——每个分片都是独立乐器,既要保证局部精准,又要实现全局和谐。

好的分库分表方案不是技术参数的堆砌,而是对业务深刻理解后的架构表达



作者:天天摸鱼的java工程师
来源:juejin.cn/post/7519688814395719714

0 个评论

要回复文章请先登录注册