前言
很多人都對分庫分表這個話題感興趣,究其緣由,可能主要是因為這技術(shù)聽起來很牛逼,高大上的樣子,了解下面試時也能裝裝逼。其次才是有部分人確實要做相關(guān)的技術(shù)調(diào)研。
本文會對分庫分表做比較系統(tǒng)的論述,看完本文,保證你在腦中有一個分庫分表的大體方案,面試官直呼666。
分庫分表時機
分庫分表雖然常放在一起說,但其實是為了解決兩部分需求。
分庫是為了解決單庫的資源瓶頸。比如磁盤,cpu,內(nèi)存,連接數(shù)等等
分表是為了解決單表數(shù)據(jù)量過大的問題。單表數(shù)據(jù)量過大會導(dǎo)致索引樹過大,即使能走索引sql性能也不會太高,還有大表DDL問題。就個人經(jīng)驗而言,對于Mysql來說,單行大小不超過50kb,數(shù)據(jù)量超過1000w就應(yīng)該分表;單行大小超過100kb,數(shù)據(jù)量500w左右就應(yīng)該分表。
分庫分表方案
分庫分表方案其實和單純的分表方案考慮點很類似,為了畫圖和描述簡潔,下面圍繞分表方案討論。
當(dāng)我們要開始考慮分表方案時,腦子里要有一些思考點:
- 數(shù)據(jù)切分方式
- 數(shù)據(jù)切分維度
- 數(shù)據(jù)擴容
- 客戶端路由還是代理層路由
數(shù)據(jù)切分方式
range切分方式
range切分也就是按范圍切分。比如id在0~10000范圍放在表1,10001~20000范圍放在表2。同理也可以按時間范圍拆分。這樣切分的優(yōu)點就是擴展性特別好,我們只要提前創(chuàng)建日后要用到的表就可以了,完全不用遷移數(shù)據(jù)。
range切分方式在實際中用的比較少,主要原因是數(shù)據(jù)均衡問題太嚴(yán)重。比如按時間切分,請求集中在新數(shù)據(jù)上,導(dǎo)致某一表過熱?;蛘弑热鐁ange范圍是1000w,新表剛開始可能只有很少的數(shù)據(jù)量,這樣數(shù)據(jù)就不均衡。
按時間range切分是單表數(shù)據(jù)過大的一種過度方案,也是我們常說的冷熱數(shù)據(jù)隔離。也許你會有印象,某段時間的淘寶和京東只能查看最近三個月的訂單。再往后的訂單就要去歷史訂單列表按日期范圍查詢。
hash切分方式
hash切分簡單來說就是根據(jù)路由key取模,數(shù)據(jù)分布很均勻。缺點是數(shù)據(jù)擴容時比較麻煩,需要遷移數(shù)據(jù)。并且為了實現(xiàn)高擴展性,可能會導(dǎo)致整體架構(gòu)比較復(fù)雜。
數(shù)據(jù)切分維度
實際生產(chǎn)中我們一個業(yè)務(wù)表可能會有很多查詢維度。如何滿足各個維度的查詢需求是個必須考慮而且又很麻煩的問題。
訂單業(yè)務(wù)是典型的多維度場景,我們就以訂單業(yè)務(wù)為例子,介紹下該問題的解決思路和方案。
關(guān)注訂單表的角色有用戶,商家和后臺運營人員。訂單表的數(shù)據(jù)結(jié)構(gòu)大致如下:
Order(oid, buyer_uid, seller_uid, time,...);
其中:
(1)oid為訂單ID,主鍵;
(2)buyer_uid為買家uid;
(3)seller_uid為賣家uid;
(4)time, 相當(dāng)于訂單的各種時間屬性;
如果數(shù)據(jù)按照oid來切分,buyer_uid和seller_uid維度的查詢需要遍歷多個庫;如果按照buyer_uid來切分,seller_uid的查詢需要遍歷多個庫;seller_uid切分類似。似乎沒有萬全的方案。
我們先來分析下查詢需求。
第一類,前臺訪問,典型的有三類需求:
(1)訂單實體查詢:通過oid查詢訂單實體,90%流量屬于這類需求;
(2)用戶訂單列表查詢:通過buyer_uid分頁查詢用戶歷史訂單列表,9%流量屬于這類需求;
(3)商家訂單列表查詢:通過seller_uid分頁查詢商家歷史訂單列表,1%流量屬于這類需求;
前臺訪問的特點是什么呢?
吞吐量大,服務(wù)要求高可用,用戶對訂單的訪問一致性要求高,商家對訂單的訪問一致性要求相對較低,可以接受一定時間的延時。
第二類,后臺訪問,根據(jù)產(chǎn)品、運營需求,訪問模式各異:
(1)按照時間,價格,商品,詳情來進行查詢;
后臺訪問的特點是什么呢?
運營側(cè)的查詢基本上是批量分頁的查詢,由于是內(nèi)部系統(tǒng),訪問量很低,對可用性的要求不高,對一致性的要求也沒這么嚴(yán)格,允許秒級甚至十秒級別的查詢延時。
我們可以根據(jù)前臺和后臺查詢需求的差異性,我們采用前后臺分離架構(gòu)。

這樣好處很多:
- server層和存儲層都分離,可以避免后臺業(yè)務(wù)影響前臺
- 前后臺面向的角色不一樣,業(yè)務(wù)領(lǐng)域很大可能存在差異,前后臺分離讓各端都只用關(guān)心自己的業(yè)務(wù)領(lǐng)域
- 前后臺查詢需求差異,各端可以各自做技術(shù)選型和設(shè)計,互不影響
按照上述架構(gòu)圖,可以滿足后臺查詢需求。咱們接著探討前臺查詢需求。我們逐個分析前臺查詢的多維度需求。
(1)oid維度和buyer_uid維度。一對多的業(yè)務(wù)場景。一對多最有效率的做法是基因法。
什么是基因法?
一個數(shù)取余2的n次方,那么余數(shù)就是這個數(shù)的二進制的最后n位數(shù)。所有我們可以位操作符把高位清零就可以得到余數(shù)。
int mod = number & ~(-1 << n)
所以,n的取舍關(guān)系到分庫的數(shù)量或者分表的數(shù)量,即2^n 個庫或表。故我們把二進制的最后n位數(shù),即上述代碼中的mod稱為分庫分表因子。
所以,需要生成的新id只要最后末尾放入分庫或分表因子就達到了我們的目的。
回到例子,通過buyer_uid分庫,假設(shè)分為16個庫,采用buyer_uid%16的方式來進行數(shù)據(jù)庫路由,所謂的模16,其本質(zhì)是buyer_uid的最后4個bit決定這行數(shù)據(jù)落在哪個庫上,這4個bit,就是分庫基因。
在訂單數(shù)據(jù)oid生成時,oid末端加入分庫基因,讓同一個buyer_uid下的所有訂單都含有相同基因,落在同一個分庫上。

(2)buyer_id維度和seller_uid維度。多對多的業(yè)務(wù)場景。多對多的業(yè)務(wù)場景只能用數(shù)據(jù)冗余法。也就是同步一份數(shù)據(jù)專門給seller_uid維度的需求使用。數(shù)據(jù)同步的方法有很多,代碼層面實時同步同步,代碼層面實時異步同步,離線同步,binlog同步等。binglog同步是最推薦的,目前業(yè)界主流的canal, databus都很成熟,但是還是有一些技術(shù)門檻的,還是要根據(jù)業(yè)務(wù)和現(xiàn)狀選擇方案。
訂單這個例子算是分析完了。多維度切分問題還有一種常用的方案——映射表方案。表中維護著路由key和其他key的映射關(guān)系,處理其他key維度的查詢過程大致如下:
- 在映射表中找到該key對應(yīng)的路由key
- 根據(jù)拿著路由key,根據(jù)路由算法找到對應(yīng)的庫/表
- 用路由key去相應(yīng)的庫/表中查數(shù)據(jù)
- 結(jié)果返回給客戶端
映射表方案本身也有潛在的單表數(shù)據(jù)量過大的問題。所以,如果我們對數(shù)據(jù)的總量有個比較明確的認(rèn)識,那么可以考慮用映射表方案。比如說像用戶表業(yè)務(wù),表主要字段如下:
user(uid, email, mobile, username,...);
一般用戶登錄場景既可以通過mobile_no,又可以通過email,還可以通過username進行登錄。但是一些用戶相關(guān)的API,又都包含user_id,那么可能需要根據(jù)這4個column都進行分庫分表。
基因法對于這個例子不可行,因為沒有合適的字段提取基因因子。這四個維度都是面向用戶的,數(shù)據(jù)冗余法不太合適,把問題復(fù)雜化了。
對于大部分公司來說有幾億用戶數(shù)據(jù)已經(jīng)是極限了。幾億數(shù)據(jù)的映射表Mysql還是能抗的。映射表方案是可行的。
數(shù)據(jù)擴容
擴容是另一個很麻煩的問題。傳統(tǒng)的擴容方案往往需要掛公告停機遷移數(shù)據(jù)。這里介紹一種不停機擴容方案——雙倍擴容方案。

如上圖,我們要把原本的兩個庫擴容為四個庫,總體來說需要三步。
- 新建db2,作為db0從庫同步數(shù)據(jù);新建db3,作為db1從庫同步數(shù)據(jù)
2.當(dāng)db2和db3數(shù)據(jù)同步完成后,去掉主從關(guān)系,并且修改client路由配置并reload。由于是成倍擴容,所以
原 ID%2=0 => db0 相當(dāng)于 ID%4=0 => db0, ID%4=2 => db2;
原 ID%2=1 => db1相當(dāng)于 ID%4=1 => db1, ID%4=3 => db3。
3.這一步之前,db0,db2有原db0的所有數(shù)據(jù);db1,db3有原db1的所有數(shù)據(jù)。擇機去除db0,db2,db1,db3多余的數(shù)據(jù)。
擴容完成,期間最多需要client重啟一下reload新的路由配置。
客戶端路由還是代理層路由
本節(jié)只從架構(gòu)思想角度論述下兩種方案的優(yōu)劣,不打算介紹具體中間件。我一直認(rèn)為架構(gòu)思想是最重要的,各種中間件和語言都只是思想的具體呈現(xiàn)。
- 客戶端路由
優(yōu)點:
a. 架構(gòu)簡單
b. 要求業(yè)務(wù)方必須了解分庫分表的細(xì)節(jié)
缺點:
a. 客戶端配置稍微復(fù)雜些
b. 分庫分表方案變動,業(yè)務(wù)方需要調(diào)整配置甚至代碼 - 代理層路由
優(yōu)點:
a. 客戶端對分庫分表無感知,像訪問一個表一樣訪問多個表
缺點:
a. 客戶端和數(shù)據(jù)庫之前多了一層代理層,性能會略受影響,架構(gòu)復(fù)雜度略微上升
b. 客戶端對分庫分表無感知,業(yè)務(wù)方程序尤其是新人可能以為是在操作一個表,這可能會導(dǎo)致業(yè)務(wù)方程序員犯一些嚴(yán)重的錯誤。(我一直認(rèn)為對于偏業(yè)務(wù)的需求,基礎(chǔ)架構(gòu)部門不要替業(yè)務(wù)部門做太多事情,很多時候業(yè)務(wù)方程序員有必要去了解相關(guān)細(xì)節(jié))
為什么不推薦分區(qū)表?
不推薦分區(qū)表主要是因為分區(qū)表如下的三個特性:
- Mysql 在第一次打開分區(qū)表的時候,需要訪問所有的分區(qū);
- 在 Mysql server 層,認(rèn)為這是同一張表,因此所有分區(qū)共用同一個 MDL 鎖;
這意味著當(dāng)我對其中一個分區(qū)做DDL操作時,所有分區(qū)都會加MDL鎖。 - 在引擎層,認(rèn)為這是不同的表,因此 非MDL 鎖,會根據(jù)分區(qū)表規(guī)則,只訪問必要的分區(qū),也就是只可能鎖訪問的分區(qū)。
- 應(yīng)用層認(rèn)為這是同一張表,所以業(yè)務(wù)程序員可能由于不了解分區(qū)表細(xì)節(jié)而犯錯。