前言
很多人都對分庫分表這個話題感興趣,究其緣由,可能主要是因為這技術(shù)聽起來很牛逼须肆,高大上的樣子匿乃,了解下面試時也能裝裝逼。其次才是有部分人確實要做相關(guān)的技術(shù)調(diào)研豌汇。
本文會對分庫分表做比較系統(tǒng)的論述幢炸,看完本文,保證你在腦中有一個分庫分表的大體方案瘤礁,面試官直呼666阳懂。
分庫分表時機
分庫分表雖然常放在一起說,但其實是為了解決兩部分需求柜思。
分庫是為了解決單庫的資源瓶頸岩调。比如磁盤,cpu赡盘,內(nèi)存号枕,連接數(shù)等等
分表是為了解決單表數(shù)據(jù)量過大的問題。單表數(shù)據(jù)量過大會導致索引樹過大陨享,即使能走索引sql性能也不會太高葱淳,還有大表DDL問題。就個人經(jīng)驗而言抛姑,對于Mysql來說赞厕,單行大小不超過50kb,數(shù)據(jù)量超過1000w就應(yīng)該分表定硝;單行大小超過100kb皿桑,數(shù)據(jù)量500w左右就應(yī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ù)均衡問題太嚴重漓摩。比如按時間切分裙士,請求集中在新數(shù)據(jù)上,導致某一表過熱管毙⊥茸担或者比如range范圍是1000w桌硫,新表剛開始可能只有很少的數(shù)據(jù)量,這樣數(shù)據(jù)就不均衡啃炸。
按時間range切分是單表數(shù)據(jù)過大的一種過度方案铆隘,也是我們常說的冷熱數(shù)據(jù)隔離。也許你會有印象南用,某段時間的淘寶和京東只能查看最近三個月的訂單膀钠。再往后的訂單就要去歷史訂單列表按日期范圍查詢。
hash切分方式
hash切分簡單來說就是根據(jù)路由key取模裹虫,數(shù)據(jù)分布很均勻肿嘲。缺點是數(shù)據(jù)擴容時比較麻煩,需要遷移數(shù)據(jù)筑公。并且為了實現(xiàn)高擴展性雳窟,可能會導致整體架構(gòu)比較復雜。
數(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, 相當于訂單的各種時間屬性怔接;
如果數(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)企垦,訪問量很低,對可用性的要求不高晒来,對一致性的要求也沒這么嚴格钞诡,允許秒級甚至十秒級別的查詢延時。
我們可以根據(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ù)的總量有個比較明確的認識况凉,那么可以考慮用映射表方案。比如說像用戶表業(yè)務(wù)各拷,表主要字段如下:
user(uid, email, mobile, username,...);
一般用戶登錄場景既可以通過mobile_no刁绒,又可以通過email,還可以通過username進行登錄烤黍。但是一些用戶相關(guān)的API知市,又都包含user_id,那么可能需要根據(jù)這4個column都進行分庫分表速蕊。
基因法對于這個例子不可行嫂丙,因為沒有合適的字段提取基因因子。這四個維度都是面向用戶的规哲,數(shù)據(jù)冗余法不太合適跟啤,把問題復雜化了。
對于大部分公司來說有幾億用戶數(shù)據(jù)已經(jīng)是極限了唉锌。幾億數(shù)據(jù)的映射表Mysql還是能抗的隅肥。映射表方案是可行的。
數(shù)據(jù)擴容
擴容是另一個很麻煩的問題袄简。傳統(tǒng)的擴容方案往往需要掛公告停機遷移數(shù)據(jù)武福。這里介紹一種不停機擴容方案——雙倍擴容方案。
如上圖痘番,我們要把原本的兩個庫擴容為四個庫捉片,總體來說需要三步。
- 新建db2汞舱,作為db0從庫同步數(shù)據(jù)伍纫;新建db3,作為db1從庫同步數(shù)據(jù)
2.當db2和db3數(shù)據(jù)同步完成后昂芜,去掉主從關(guān)系莹规,并且修改client路由配置并reload。由于是成倍擴容泌神,所以
原 ID%2=0 => db0 相當于 ID%4=0 => db0, ID%4=2 => db2良漱;
原 ID%2=1 => db1相當于 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)劣,不打算介紹具體中間件蒋失。我一直認為架構(gòu)思想是最重要的返帕,各種中間件和語言都只是思想的具體呈現(xiàn)。
- 客戶端路由
優(yōu)點:
a. 架構(gòu)簡單
b. 要求業(yè)務(wù)方必須了解分庫分表的細節(jié)
缺點:
a. 客戶端配置稍微復雜些
b. 分庫分表方案變動篙挽,業(yè)務(wù)方需要調(diào)整配置甚至代碼 - 代理層路由
優(yōu)點:
a. 客戶端對分庫分表無感知荆萤,像訪問一個表一樣訪問多個表
缺點:
a. 客戶端和數(shù)據(jù)庫之前多了一層代理層,性能會略受影響铣卡,架構(gòu)復雜度略微上升
b. 客戶端對分庫分表無感知观腊,業(yè)務(wù)方程序尤其是新人可能以為是在操作一個表,這可能會導致業(yè)務(wù)方程序員犯一些嚴重的錯誤算行。(我一直認為對于偏業(yè)務(wù)的需求梧油,基礎(chǔ)架構(gòu)部門不要替業(yè)務(wù)部門做太多事情,很多時候業(yè)務(wù)方程序員有必要去了解相關(guān)細節(jié))
為什么不推薦分區(qū)表州邢?
不推薦分區(qū)表主要是因為分區(qū)表如下的三個特性:
- Mysql 在第一次打開分區(qū)表的時候儡陨,需要訪問所有的分區(qū);
- 在 Mysql server 層量淌,認為這是同一張表骗村,因此所有分區(qū)共用同一個 MDL 鎖;
這意味著當我對其中一個分區(qū)做DDL操作時呀枢,所有分區(qū)都會加MDL鎖胚股。 - 在引擎層,認為這是不同的表裙秋,因此 非MDL 鎖琅拌,會根據(jù)分區(qū)表規(guī)則,只訪問必要的分區(qū)摘刑,也就是只可能鎖訪問的分區(qū)进宝。
- 應(yīng)用層認為這是同一張表,所以業(yè)務(wù)程序員可能由于不了解分區(qū)表細節(jié)而犯錯枷恕。