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

1,410 阅读6分钟

订单表超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亿级订单如同指挥一场交响乐——每个分片都是独立乐器,既要保证局部精准,又要实现全局和谐。
好的分库分表方案不是技术参数的堆砌,而是对业务深刻理解后的架构表达