10亿订单分库分表

场景痛点:某电商平台的MySQL订单表达到7亿行时,出现致命问题:

复制代码
-- 简单查询竟需12秒!
SELECT * FROM orders WHERE user_id=10086 LIMIT 10;

-- 统计全表耗时278秒
SELECT COUNT(*) FROM orders;

核心矛盾

  1. B+树索引深度达到5层,磁盘IO暴增。
  2. 单表超200GB导致备份时间窗突破6小时。
  3. 写并发量达8000QPS,主从延迟高达15分钟。

关键认知 :当单表数据量突破5000万行时,就该启动分库分表设计预案。

那么问题来了,假如现在有10亿的订单数据,我们该如何做分库分表呢?

今天这篇文章就跟大家一起聊聊这个问题,希望对你会有所帮助。

1 分库分表核心策略

1.1 垂直拆分:先给数据做减法

优化效果

  • 核心表体积减少60%
  • 高频查询字段集中提升缓存命中率

1.2 水平拆分:终极解决方案

分片键选择三原则

  1. 离散性:避免数据热点(如user_id优于status)
  2. 业务相关性:80%查询需携带该字段
  3. 稳定性:值不随业务变更(避免使用手机号)

分片策略对比

策略类型 适用场景 扩容复杂度 示例
范围分片 带时间范围的查询 简单 create_time按月分表
哈希取模 均匀分布 困难 user_id % 128
一致性哈希 动态扩容 中等 使用Ketama算法
基因分片 避免跨分片查询 复杂 从user_id提取分库基因

2 基因分片

针对订单系统的三大高频查询:

  1. 用户查历史订单(user_id)
  2. 商家查订单(merchant_id)
  3. 客服按订单号查询(order_no)

解决方案

Snowflake订单ID改造

复制代码
// 基因分片ID生成器
public class OrderIdGenerator {
    // 64位ID结构:符号位(1)+时间戳(41)+分片基因(12)+序列号(10)
    private static final int GENE_BITS = 12;
    
    public static long generateId(long userId) {
        long timestamp = System.currentTimeMillis() - 1288834974657L;
        // 提取用户ID后12位作为基因
        long gene = userId & ((1 << GENE_BITS) - 1); 
        long sequence = ... // 获取序列号
        
        return (timestamp << 22) 
             | (gene << 10) 
             | sequence;
    }
    
    // 从订单ID反推分片位置
    public static int getShardKey(long orderId) {
        return (int) ((orderId >> 10) & 0xFFF); // 提取中间12位
    }
}

路由逻辑

复制代码
// 分库分表路由引擎
public class OrderShardingRouter {
    // 分8个库 每个库16张表
    private static final int DB_COUNT = 8; 
    private static final int TABLE_COUNT_PER_DB = 16;
    
    public static String route(long orderId) {
        int gene = OrderIdGenerator.getShardKey(orderId);
        int dbIndex = gene % DB_COUNT;
        int tableIndex = gene % TABLE_COUNT_PER_DB;
        
        return "order_db_" + dbIndex + ".orders_" + tableIndex;
    }
}

关键突破:通过基因嵌入,使相同用户的订单始终落在同一分片,同时支持通过订单ID直接定位分片

3 跨分片查询

3.1 异构索引表方案

Elasticsearch索引表结构

复制代码
{
  "order_index": {
    "mappings": {
      "properties": {
        "order_no": { "type": "keyword" },
        "shard_key": { "type": "integer" },
        "create_time": { "type": "date" }
      }
    }
  }
}
4.2 全局二级索引(GSI)
复制代码
-- 在ShardingSphere中创建全局索引
CREATE SHARDING GLOBAL INDEX idx_merchant ON orders(merchant_id) 
    BY SHARDING_ALGORITHM(merchant_hash) 
    WITH STORAGE_UNIT(ds_0,ds_1);

4、数据迁移

双写迁移方案

灰度切换步骤

  1. 开启双写(新库写失败需回滚旧库)
  2. 全量迁移历史数据(采用分页批处理)
  3. 增量数据实时校验(校验不一致自动修复)
  4. 按用户ID灰度流量切换(从1%到100%)

5、避坑指南

5.1 热点问题

双十一期间发现某网红店铺订单全部分到同一分片。

解决方案:引入复合分片键 (merchant_id + user_id) % 1024

5.2 分布式事务

这里的分布式事务使用的RocketMQ的数据最终一致性方案:

复制代码
// 最终一致性方案
@Transactional
public void createOrder(Order order) {
   orderDao.insert(order); // 写主库
   rocketMQTemplate.sendAsync("order_create_event", order); // 发消息
}

// 消费者处理
@RocketMQMessageListener(topic = "order_create_event")
public void handleEvent(OrderEvent event) {
   bonusService.addPoints(event.getUserId()); // 异步加积分
   inventoryService.deduct(event.getSkuId()); // 异步扣库存
}

5.3 分页陷阱

跨分片查询页码错乱。

解决方案:改用ES聚合查询或业务折衷方案(只查最近3个月订单)。

6 终极架构方案

性能指标

场景 拆分前 拆分后
用户订单查询 3200ms 68ms
商家订单导出 超时失败 8s完成
全表统计 不可用 1.2s(近似)

总结

  1. 分片键选择大于努力:基因分片是订单系统的最佳拍档。
  2. 扩容预留空间:建议初始设计支持2年数据增长。
  3. 避免过度设计:小表关联查询远比分布式Join高。效
  4. 监控驱动优化:重点关注分片倾斜率>15%的库。
相关推荐
亭台烟雨中1 个月前
简单实现shardingSphere + MybatisPlus分库分表2025
java·数据库·分库分表
猿与禅1 个月前
分库分表的取舍
shardingsphere·分库分表
南客先生3 个月前
海量聊天数据处理:基于Spring Boot与SharingJDBC的分库分表策略及ClickHouse冷热数据分离
mysql·clickhouse·springboot·分库分表·大数据处理·sharingjdbc
红豆和绿豆4 个月前
分库分表中间件开源
分库分表
fajianchen5 个月前
分库分表后如何进行join操作
sql·分库分表
芝法酱6 个月前
芝法酱学习笔记(2.3)——shardingsphere分库分表
shardingsphere·分库分表
JingAi_jia9176 个月前
【源码】Sharding-JDBC源码分析之SQL中影子库ShadowSQLRouter路由的原理
分库分表·sharding-jdbc·springboot分库分表·影子库规则·shadowsqlrouter
技术路上的苦行僧6 个月前
分布式专题(10)之ShardingSphere分库分表实战指南
分布式·shardingsphere·分库分表
ezreal_pan7 个月前
ShardingSphere-Proxy 连接实战:从 Golang 原生 SQL 到 GORM 的应用
golang·shardingsphere·分库分表