10億訂單如何做分庫分表?
前言
場景痛點:某電商平臺的MySQL訂單表達到7億行時,出現致命問題:
-- 簡單查詢竟需12秒!
SELECT * FROM orders WHERE user_id=10086 LIMIT 10;
-- 統計全表耗時278秒
SELECT COUNT(*) FROM orders;
核心矛盾:
- B+樹索引深度達到5層,磁盤IO暴增。
- 單表超200GB導致備份時間窗突破6小時。
- 寫并發量達8000QPS,主從延遲高達15分鐘。
關鍵認知:當單表數據量突破5000萬行時,就該啟動分庫分表設計預案。
那么問題來了,假如現在有10億的訂單數據,我們該如何做分庫分表呢?
今天這篇文章就跟大家一起聊聊這個問題,希望對你會有所幫助。
1.分庫分表核心策略
1.1 垂直拆分:先給數據做減法
圖片
優化效果:
- 核心表體積減少60%
- 高頻查詢字段集中提升緩存命中率
1.2 水平拆分:終極解決方案
分片鍵選擇三原則:
- 離散性:避免數據熱點(如user_id優于status)
- 業務相關性:80%查詢需攜帶該字段
- 穩定性:值不隨業務變更(避免使用手機號)
分片策略對比:
策略類型 | 適用場景 | 擴容復雜度 | 示例 |
范圍分片 | 帶時間范圍的查詢 | 簡單 | create_time按月分表 |
哈希取模 | 均勻分布 | 困難 | user_id % 128 |
一致性哈希 | 動態擴容 | 中等 | 使用Ketama算法 |
基因分片 | 避免跨分片查詢 | 復雜 | 從user_id提取分庫基因 |
2.基因分片
針對訂單系統的三大高頻查詢:
- 用戶查歷史訂單(user_id)
- 商家查訂單(merchant_id)
- 客服按訂單號查詢(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.數據遷移
雙寫遷移方案:
灰度切換步驟:
- 開啟雙寫(新庫寫失敗需回滾舊庫)
- 全量遷移歷史數據(采用分頁批處理)
- 增量數據實時校驗(校驗不一致自動修復)
- 按用戶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(近似) |
總結
- 分片鍵選擇大于努力:基因分片是訂單系統的最佳拍檔。
- 擴容預留空間:建議初始設計支持2年數據增長。
- 避免過度設計:小表關聯查詢遠比分布式Join高。效
- 監控驅動優化:重點關注分片傾斜率>15%的庫。
真正的架構藝術,是在分與合之間找到平衡點。