MySQL分庫分表的聚合問題踩坑實(shí)錄
一:問題的引入
你們公司的訂單表有 2 億數(shù)據(jù),怎么做的分庫分表?
如果我們按用戶 ID(user_id)取模,分了 16 個(gè)庫,每個(gè)庫 64 張表,一共 1024 張表。這樣用戶查自己的訂單特別快,直接定位到具體的表。
此時(shí)問題就來了,那商家(Seller)要查自己店鋪的訂單列表怎么辦? 商家又沒有 user_id,按你的分法,商家查一次豈不是要掃描全部 1024 張表?這系統(tǒng)能不崩?
# ds0 是數(shù)據(jù)源名稱,db0 是實(shí)際的數(shù)據(jù)庫名稱。在配置中:
# ds0 是一個(gè)邏輯名稱,代表一個(gè)數(shù)據(jù)庫連接配置
# db0 是 MySQL 中真實(shí)的數(shù)據(jù)庫名
databaseName: order_system
dataSources:
ds0:
url: jdbc:mysql://localhost:3306/db0
username: root
password: ******
ds1:
url: jdbc:mysql://localhost:3306/db1
username: root
password: ******
# ... 一直到 ds15,共16個(gè)數(shù)據(jù)源
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds$->{0..15}.t_order_$->{0..63}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: database_inline
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: table_inline
shardingAlgorithms:
database_inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 16}
table_inline:
type: INLINE
props:
algorithm-expression: t_order_$->{user_id % 64}
內(nèi)部執(zhí)行情況如下:假設(shè)我們要插入一條訂單數(shù)據(jù):
INSERT INTO t_order (order_id, user_id, amount) VALUES (10001, 12345, 299.00);
第1步:ShardingSphere 接收 SQL
應(yīng)用層發(fā)送 SQL 到 ShardingSphere,它作為中間件攔截了這個(gè) SQL。
第2步:分片路由計(jì)算
根據(jù)配置的分片算法計(jì)算:
user_id = 12345 // 計(jì)算目標(biāo)庫:user_id % 16 = 12345 % 16 = 9 // 所以目標(biāo)庫是 ds9 // 計(jì)算目標(biāo)表:user_id % 64 = 12345 % 64 = 57 // 所以目標(biāo)表是 t_order_57
第3步:SQL 路由改寫
ShardingSphere 將原始 SQL 改寫為:
-- 原SQL INSERT INTO t_order (order_id, user_id, amount) VALUES (10001, 12345, 299.00); -- 改寫后的SQL INSERT INTO ds9.t_order_57 (order_id, user_id, amount) VALUES (10001, 12345, 299.00);
第4步:查找數(shù)據(jù)源配置
ShardingSphere 查找 ds9 對(duì)應(yīng)的實(shí)際數(shù)據(jù)庫連接:
ds9: # 邏輯數(shù)據(jù)源 url: jdbc:mysql://localhost:3306/db9 # 實(shí)際數(shù)據(jù)庫是 db9 username: root password: ******
第5步:執(zhí)行 SQL
ShardingSphere 連接到 localhost:3306/db9 數(shù)據(jù)庫,執(zhí)行:
INSERT INTO t_order_57 (order_id, user_id, amount) VALUES (10001, 12345, 299.00);
第6步:返回結(jié)果
數(shù)據(jù)庫執(zhí)行成功,返回結(jié)果給 ShardingSphere,再返回給應(yīng)用。
| 邏輯數(shù)據(jù)源 | 實(shí)際數(shù)據(jù)庫 | 包含的表 |
|---|---|---|
| ds0 | db0 | t_order_0 ~ t_order_63 |
| ds1 | db1 | t_order_0 ~ t_order_63 |
| ds2 | db2 | t_order_0 ~ t_order_63 |
| … | … | … |
| ds9 | db9 | t_order_0 ~ t_order_63 |
| … | … | … |
| ds15 | db15 | t_order_0 ~ t_order_63 |
二:切開后怎么聚合
所以分庫分表不僅僅是“把數(shù)據(jù)切開”這么簡單,難點(diǎn)永遠(yuǎn)在于 “切開后怎么聚合”
1:什么時(shí)候該分
面試?yán)飭?ldquo;什么時(shí)候分庫分表”,很多人上來就背:“阿里開發(fā)手冊(cè)說單表超過 500 萬行或者 2GB 就要分……”,顯然過于教條了,現(xiàn)在的現(xiàn)在的硬件(SSD + 大內(nèi)存),單表跑個(gè) 1000 萬數(shù)據(jù),索引建好了照樣飛快。
所有真正逼你分庫分表的,通常不是 “存儲(chǔ)容量”,而是 “連接數(shù)” 和 “維護(hù)成本”:
- 連接數(shù)瓶頸:一個(gè) MySQL 實(shí)例的連接數(shù)是有限的(通常幾千個(gè))。當(dāng)并發(fā) QPS 極高,所有請(qǐng)求都打到一個(gè)主庫,數(shù)據(jù)庫連接池瞬間被打爆,這時(shí)候必須 “分庫” 來分?jǐn)偛l(fā)寫壓力。
- DDL 痛苦:給一張 5000 萬行的表加個(gè)字段,你試試?鎖表能鎖到你懷疑人生,業(yè)務(wù)直接停擺。這時(shí)候必須 “分表” 來降低單表大小。
DDL:動(dòng)結(jié)構(gòu)(Create、Alter、Drop) 操作后自動(dòng)提交,不能回滾,影響數(shù)據(jù)庫結(jié)構(gòu) DML:動(dòng)數(shù)據(jù)(Insert、Update、Delete) 操作表里的數(shù)據(jù),可以回滾(在事務(wù)中) DQL:查數(shù)據(jù)(Select) 最常用的操作,不修改數(shù)據(jù) DCL:控權(quán)限(Grant、Revoke) TCL:管事務(wù)(Commit、Rollback)
2:多維查詢的三種解法
2.1:異構(gòu)索引表(雙寫冗余)
既然切分維度不能兼顧,那就 用空間換時(shí)間

- C 端(用戶視角) :主庫依然按 user_id % 16 分片。用戶查訂單,快如閃電。
- B 端(商家視角) :再搞一套“商家?guī)?rdquo;,數(shù)據(jù)按 merchant_id % 16 分片。
- 同步機(jī)制:用戶下單寫入“用戶庫”成功后,異步把數(shù)據(jù)同步一份到“商家?guī)?rdquo;。
這種方案需要配置兩個(gè)獨(dú)立的 ShardingSphere 規(guī)則,一個(gè)用于用戶庫,一個(gè)用于商家?guī)欤?/p>
# shardingsphere-config.yaml
databaseName: order_system
dataSources:
# 用戶庫數(shù)據(jù)源 (16個(gè))
user_ds0:
url: jdbc:mysql://localhost:3306/user_db0
username: root
password: ******
user_ds1:
url: jdbc:mysql://localhost:3306/user_db1
username: root
password: ******
# ... user_ds2 ~ user_ds15
# 商家?guī)鞌?shù)據(jù)源 (16個(gè))
merchant_ds0:
url: jdbc:mysql://localhost:3306/merchant_db0
username: root
password: ******
merchant_ds1:
url: jdbc:mysql://localhost:3306/merchant_db1
username: root
password: ******
# ... merchant_ds2 ~ merchant_ds15
rules:
- !SHARDING
tables:
# 用戶視角的訂單表配置
t_order_user:
actualDataNodes: user_ds$->{0..15}.t_order_user_$->{0..63}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: user_db_inline
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: user_table_inline
# 商家視角的訂單表配置
t_order_merchant:
actualDataNodes: merchant_ds$->{0..15}.t_order_merchant_$->{0..63}
databaseStrategy:
standard:
shardingColumn: merchant_id
shardingAlgorithmName: merchant_db_inline
tableStrategy:
standard:
shardingColumn: merchant_id
shardingAlgorithmName: merchant_table_inline
shardingAlgorithms:
# 用戶庫分片算法
user_db_inline:
type: INLINE
props:
algorithm-expression: user_ds$->{user_id % 16}
user_table_inline:
type: INLINE
props:
algorithm-expression: t_order_user_$->{user_id % 64}
# 商家?guī)旆制惴?
merchant_db_inline:
type: INLINE
props:
algorithm-expression: merchant_ds$->{merchant_id % 16}
merchant_table_inline:
type: INLINE
props:
algorithm-expression: t_order_merchant_$->{merchant_id % 64}@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Transactional
public void createOrder(Order order) {
// 1. 寫入用戶庫(主庫)
orderMapper.insertToUserDb(order);
// 2. 發(fā)送消息,異步同步到商家?guī)?
rabbitTemplate.convertAndSend("order.exchange", "order.sync", order);
}
}
@Component
@Slf4j
public class OrderSyncConsumer {
@Autowired
private MerchantOrderMapper merchantOrderMapper;
@RabbitListener(queues = "order.sync.queue")
public void syncToMerchantDb(Order order) {
try {
// 3. 異步寫入商家?guī)?
merchantOrderMapper.insertToMerchantDb(order);
} catch (Exception e) {
log.error("同步訂單到商家?guī)焓?, e);
// 記錄失敗日志,后續(xù)補(bǔ)償處理
}
}
}
如果消息發(fā)送失敗,或者消費(fèi)者掛了,商家?guī)熵M不是一直少訂單?
為了保證最終一致性,我們通常采用 RocketMQ 事務(wù)消息 或 本地事務(wù)表 + 定時(shí)輪詢 模式
- 事務(wù)消息: 利用 MQ 的半消息機(jī)制,確保‘本地訂單入庫’和‘消息發(fā)送’要么同時(shí)成功,要么同時(shí)失敗。
- 兜底重試: 配合定時(shí)任務(wù)掃描未確認(rèn)的消息,確保 At Least Once(至少投遞一次) ,保證商家?guī)旖^對(duì)不會(huì)丟單。”
2.2:大寬表 + ElasticSearch
如果運(yùn)營人員要按“下單時(shí)間”、“金額”、“地區(qū)”等奇葩條件篩選,MySQL 分庫分表就徹底歇菜了。

- MySQL 只負(fù)責(zé)核心交易鏈路(存、取、改狀態(tài)),按 user_id 分片。
- 把數(shù)據(jù)通過 Canal + MQ 實(shí)時(shí)同步到 Elasticsearch (ES)。
- 商家查詢、運(yùn)營后臺(tái)查詢、復(fù)雜報(bào)表,全部走 ES。
ES 不是實(shí)時(shí)強(qiáng)一致的(通常有 1 秒延遲)。
如果商家剛收到新訂單通知,點(diǎn)進(jìn)去卻在 ES 查不到,怎么辦?
在代碼層做降級(jí)邏輯。當(dāng) ES 查不到結(jié)果(或數(shù)據(jù)明顯滯后)時(shí),系統(tǒng)會(huì)自動(dòng)降級(jí)回 MySQL 的商家異構(gòu)庫進(jìn)行點(diǎn)查。雖然后端壓力大一點(diǎn),但保證了用戶體驗(yàn)的閉環(huán)。
三:分表后SQL三大坑
1:分表后的全局 ID 怎么生成
絕對(duì)不能用自增主鍵。面試時(shí)推薦答 美團(tuán) Leaf 的號(hào)段模式。相比于雪花算法(Snowflake),它不強(qiáng)依賴機(jī)器時(shí)鐘,不會(huì)因?yàn)闀r(shí)鐘回?fù)軐?dǎo)致 ID 重復(fù),更適合嚴(yán)謹(jǐn)?shù)慕鹑诩?jí)業(yè)務(wù)。
- 數(shù)據(jù)庫自增ID:就像每次做蛋糕都要去總部申請(qǐng)一個(gè)唯一的編號(hào),做完一個(gè)申請(qǐng)一個(gè)。總部很忙,一旦總部出問題,整個(gè)蛋糕店就得停工。
- Leaf號(hào)段模式:相當(dāng)于總部一次性批發(fā)給蛋糕店一批連續(xù)編號(hào)的蛋糕券(比如 #1001-#2000)。蛋糕店自己用券賣蛋糕,速度極快。當(dāng)券只剩10%時(shí),就派個(gè)小弟異步去總部領(lǐng)下一批券(比如 #2001-#3000)。這樣,即使總部暫時(shí)聯(lián)系不上,蛋糕店也能靠手里的券繼續(xù)營業(yè)
雙buffer(雙緩存區(qū))是Leaf保證高可用和高性能的秘訣。每個(gè)業(yè)務(wù)在Leaf服務(wù)的內(nèi)存中都有兩個(gè)緩存區(qū)(Segment),當(dāng)前一個(gè)Buffer的ID快用完時(shí),會(huì)異步地去加載下一個(gè)Buffer,確保了發(fā)號(hào)過程行云流水,不會(huì)卡頓
1?? 從美團(tuán)官方GitHub倉庫克隆Leaf項(xiàng)目:https://github.com/Meituan-Dianping/Leaf
2?? 在你的MySQL中創(chuàng)建一個(gè)數(shù)據(jù)庫(例如 leaf_db),執(zhí)行建表語句并初始化
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '' COMMENT '業(yè)務(wù)標(biāo)識(shí),如 order',
`max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '當(dāng)前已分配的最大ID',
`step` int(11) NOT NULL COMMENT '號(hào)段步長,即每次批發(fā)的數(shù)量',
`description` varchar(256) DEFAULT NULL COMMENT '描述',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 初始化訂單服務(wù)的發(fā)號(hào)記錄
INSERT INTO leaf_alloc(biz_tag, max_id, step, description)
VALUES ('order', 1, 2000, '訂單ID發(fā)號(hào)器');
3?? 修改 leaf-server/src/main/resources/leaf.properties 文件,開啟號(hào)段模式并配置數(shù)據(jù)庫連接
# 開啟號(hào)段模式 leaf.segment.enable=true # 關(guān)閉雪花算法模式(兩者不能同時(shí)開啟) leaf.snowflake.enable=false # 數(shù)據(jù)庫連接配置 leaf.jdbc.url=jdbc:mysql://你的數(shù)據(jù)庫IP:3306/leaf_db?useUnicode=true&characterEncoding=utf8 leaf.jdbc.username=你的用戶名 leaf.jdbc.password=你的密碼
4?? 啟動(dòng)Leaf服務(wù)。你可以將它作為一個(gè)獨(dú)立的發(fā)號(hào)中心集群來部署,以提高可用性
2:分表后,怎么做深分頁(Limit 10000, 10)
分布式數(shù)據(jù)庫的‘內(nèi)存殺手’。中間件需要去每個(gè)分片取前 10010 條,聚合排序,性能極差
- 業(yè)務(wù)規(guī)避: 禁止跳頁,只允許‘下一頁’。
- Seek 游標(biāo)法: 利用
WHERE id < last_id LIMIT 10的方式查詢,利用索引避開 Offset 掃描。 - ES Scroll: 如果是極其復(fù)雜的深分頁,直接走 ES。”
3:擴(kuò)容怎么辦?原本 16 個(gè)庫不夠用了,要擴(kuò)到 32 個(gè)
采用 2 倍擴(kuò)容(Scale Out) ,且必須配合 在線數(shù)據(jù)遷移
- 全量同步: 將舊庫數(shù)據(jù)全量搬運(yùn)到新庫。
- 增量追平: 利用 Canal/DTS 追平遷移期間產(chǎn)生的增量數(shù)據(jù)。
- 數(shù)據(jù)校驗(yàn): 全量比對(duì)一致后。
- 切流: 短暫切斷寫入(秒級(jí)),更新路由規(guī)則,將流量切到新庫。
總結(jié)
到此這篇關(guān)于MySQL分庫分表聚合問題踩坑實(shí)錄的文章就介紹到這了,更多相關(guān)MySQL分庫分表聚合內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MySQL 文本文件的導(dǎo)入導(dǎo)出數(shù)據(jù)的方法
但有時(shí)為了更快速地插入大批量數(shù)據(jù)或交換數(shù)據(jù),需要從文本中導(dǎo)入數(shù)據(jù)或?qū)С鰯?shù)據(jù)到文本。下面的具體的方法大家可以參考下。多測試。2009-11-11
MySQL數(shù)據(jù)庫的高可用方案總結(jié)
這篇文章主要針對(duì)MySQL數(shù)據(jù)庫的高可用方案進(jìn)行詳細(xì)總結(jié),高可用架構(gòu)對(duì)于互聯(lián)網(wǎng)服務(wù)基本是標(biāo),本文是對(duì)各種方案的總結(jié),感興趣的小伙伴們可以參考一下2016-05-05
mysql計(jì)算2個(gè)日期的月份差和天數(shù)差實(shí)現(xiàn)方式
文章主要講述如何計(jì)算兩個(gè)日期之間的相隔月份和天數(shù),強(qiáng)調(diào)了日期格式的重要性,并提供了一個(gè)計(jì)算天數(shù)的示例2025-11-11
MySQL 服務(wù)器參數(shù)說明及查看 設(shè)置方法
MySQL 服務(wù)器參數(shù)說明及查看,設(shè)置方法,對(duì)于mysql不是很熟悉的朋友,可以參考下。2009-03-03

