⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 苏三说技术 「苏三」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

场景痛点:某电商平台的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生成器
publicclass OrderIdGenerator {
// 64位ID结构:符号位(1)+时间戳(41)+分片基因(12)+序列号(10)
privatestaticfinalint 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位
}
}

路由逻辑:

// 分库分表路由引擎
publicclass OrderShardingRouter {
// 分8个库 每个库16张表
privatestaticfinalint DB_COUNT = 8;
privatestaticfinalint 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. 1. 1 分库分表核心策略
    1. 1.1. 1.1 垂直拆分:先给数据做减法
    2. 1.2. 1.2 水平拆分:终极解决方案
  2. 2. 2 基因分片
  3. 3. 3 跨分片查询
    1. 3.1. 3.1 异构索引表方案
      1. 3.1.1. 4.2 全局二级索引(GSI)
  4. 4. 4、数据迁移
  5. 5. 5、避坑指南
    1. 5.1. 5.1 热点问题
    2. 5.2. 5.2 分布式事务
    3. 5.3. 5.3 分页陷阱
  6. 6. 6 终极架构方案
  7. 7. 总结