写点什么

10 亿订单如何分库分表?

  • 2025-07-04
    福建
  • 本文字数:1928 字

    阅读完需:约 6 分钟

前言


场景痛点:某电商平台的 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. 稳定性:值不随业务变更(避免使用手机号)


分片策略对比



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 的数据最终一致性方案:


// 最终一致性方案@Transactionalpublic 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 终极架构方案



性能指标



总结


  1. 分片键选择大于努力:基因分片是订单系统的最佳拍档。

  2. 扩容预留空间:建议初始设计支持 2 年数据增长。

  3. 避免过度设计:小表关联查询远比分布式 Join 高。效

  4. 监控驱动优化:重点关注分片倾斜率>15%的库。


真正的架构艺术,是在分与合之间找到平衡点。


文章转载自:苏三说技术

原文链接:https://www.cnblogs.com/12lisu/p/18963990

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2025-04-01 加入

还未添加个人简介

评论

发布
暂无评论
10亿订单如何分库分表?_Java_电子尖叫食人鱼_InfoQ写作社区