数据库拆分过程及挑战
來自:http://www.tianshouzhi.com/api/tutorials/dragon/362
互聯(lián)網(wǎng)當(dāng)下的數(shù)據(jù)庫拆分過程基本遵循的順序是:垂直拆分、讀寫分離、分庫分表(水平拆分)。每個拆分過程都能解決業(yè)務(wù)上的一些問題,但同時也面臨了一些挑戰(zhàn)。
1 垂直拆分
對于一個剛上線的互聯(lián)網(wǎng)項目來說,由于前期活躍用戶數(shù)量并不多,并發(fā)量也相對較小,所以此時企業(yè)一般都會選擇將所有數(shù)據(jù)存放在一個數(shù)據(jù)庫 中進行訪問操作。
舉例來說,對于一個電商系統(tǒng),其用戶模塊和產(chǎn)品模塊的表剛開始都是位于一個db_eshop庫中。
其中:user表和user_account表屬于用戶模塊,product_category表和product表屬于產(chǎn)品模塊
剛開始,可能公司的技術(shù)團隊規(guī)模比較小,因此整個技術(shù)團隊共同維護db_eshop庫。隨著公司業(yè)務(wù)的發(fā)展,技術(shù)團隊人員也得到了擴張,劃分為不同的技術(shù)小組,不同的小組負責(zé)不同的業(yè)務(wù)模塊。例如A小組負責(zé)用戶模塊,B小組負責(zé)產(chǎn)品模塊。此時數(shù)據(jù)庫也迎來了第一次拆分:垂直拆分。
這里的垂直拆分,指的是將一個包含了很多表的數(shù)據(jù)庫,根據(jù)表的功能的不同,拆分為多個小的數(shù)據(jù)庫,每個庫包含部分表。下圖演示將上面提到的db_eshop庫,拆分為db_user庫和db_product庫。
關(guān)于垂直拆分,還有另一種說法,將一個包含了很多字段的大表拆分為多個小表,每個表包含部分字段。而筆者認為,根據(jù)表功能的不同的對數(shù)據(jù)庫進行拆分,這種情況更加常見。
2 讀寫分離
隨著后續(xù)的市場推廣力度不斷加強,用戶數(shù)量和并發(fā)量不斷上升。這時如果僅靠一個數(shù)據(jù)庫來支撐所有訪問壓力,幾乎是在 自尋死路 。以產(chǎn)品庫為例,可能庫中包含了幾萬種商品,并且每天新增幾十種,而產(chǎn)品庫每天的訪問了可能有幾億甚至幾十億次。數(shù)據(jù)庫讀的壓力太大,單臺mysql實例扛不住,此時大部分 Mysql DBA 就會將數(shù)據(jù)庫設(shè)置成 讀寫分離狀態(tài) ,也就是一個 Master 節(jié)點(主庫)對應(yīng)多個 Salve 節(jié)點(從庫)。可以將slave節(jié)點的數(shù)據(jù)理解為master節(jié)點數(shù)據(jù)的全量備份。
master節(jié)點只有一個且可讀可寫,slave節(jié)點有多個且只可以讀。新增產(chǎn)品時,應(yīng)用將數(shù)據(jù)寫入master主庫,主庫將數(shù)據(jù)同步給多個slave從庫。當(dāng)查詢產(chǎn)品時,應(yīng)用選擇某個salve節(jié)點讀取數(shù)據(jù)。
讀寫分離的優(yōu)點:
? 這樣通過配置多個slave節(jié)點,可以有效的避免過大的訪問量對單個庫造成的壓力。
讀寫分離的挑戰(zhàn):
1、對于DBA而言,需要配置數(shù)據(jù)庫主從同步
? 關(guān)于如何配置數(shù)據(jù)庫的主從同步,這個目前方案已經(jīng)很成熟。以mysql為例:
? 可以參考官方文檔:https://dev.mysql.com/doc/refman/5.7/en/replication.html,
? 筆者也寫了一篇文章介紹如何通過mysql_multi的方式配置主從同步:http://www.tianshouzhi.com/api/tutorials/mysql。
2、對于開發(fā)人員而言,必須要對sql類型進行判斷,如果是select等讀請求,就走從庫,如果是insert、update、delete等寫請求,就走主庫。此外還有一些其他的問題要考慮:
- **主從數(shù)據(jù)同步延遲問題:**因為數(shù)據(jù)是從master節(jié)點通過網(wǎng)絡(luò)同步給多個slave節(jié)點,因此必然存在延遲。因此有可能出現(xiàn)我們在master節(jié)點中已經(jīng)插入了數(shù)據(jù),但是從slave節(jié)點卻讀取不到的問題。對于一些強一致性的業(yè)務(wù)場景,要求插入后必須能讀取到,因此對于這種情況,我們需要提供一種方式,讓讀請求也可以走主庫,而主庫上的數(shù)據(jù)必然是最新的。
- **事務(wù)問題:**如果一個事務(wù)中同時包含了讀請求(如select)和寫請求(如insert),如果讀請求走從庫,寫請求走主庫,由于跨了多個庫,那么jdbc本地事務(wù)已經(jīng)無法控制,屬于分布式事務(wù)的范疇。而分布式事務(wù)非常復(fù)雜且效率較低。因此對于讀寫分離,目前主流的做法是,事務(wù)中的所有sql統(tǒng)一都走主庫,由于只涉及到一個庫,jdbc本地事務(wù)就可以搞定。
- **高可用的考慮:**例如master配置了多個slave節(jié)點,如果其中某個slave節(jié)點掛了,那么之后的讀請求,我們應(yīng)用將其轉(zhuǎn)發(fā)到正常工作的slave節(jié)點上。另外,如果新增了slave節(jié)點,應(yīng)用也應(yīng)該感知到,可以將讀請求轉(zhuǎn)發(fā)到新的slave節(jié)點上。
3 分庫分表
? 經(jīng)過垂直分區(qū)后的 Master/Salve 模式完全可以承受住難以想象的高并發(fā)訪問操作,但是否可以永遠 高枕無憂 了?答案是否定的,一旦業(yè)務(wù)表中的數(shù)據(jù)量大了,從維護和性能角度來看,無論是任何的 CRUD 操作,對于數(shù)據(jù)庫而言都是一件極其耗費資源的事情。即便設(shè)置了索引, 仍然無法掩蓋因為數(shù)據(jù)量過大從而導(dǎo)致的數(shù)據(jù)庫性能下降的事實 ,因此這個時候 Mysql DBA 或許就該對數(shù)據(jù)庫進行 水平分區(qū) (sharding,即分庫分表 )。經(jīng)過水平分區(qū)設(shè)置后的業(yè)務(wù)表,必然能夠?qū)⒃疽粡埍砭S護的海量數(shù)據(jù)分配給 N 個子表進行存儲和維護。
水平分表從具體實現(xiàn)上又可以分為3種:只分表、只分庫、分庫分表,下圖展示了這三種情況:
只分表:
? 將db庫中的user表拆分為2個分表,user_0和user_1,這兩個表還位于同一個庫中。 適用場景:如果庫中的多個表中只有某張表或者少數(shù)表數(shù)據(jù)量過大,那么只需要針對這些表進行拆分,其他表保持不變。
只分庫:
? 將db庫拆分為db_0和db_1兩個庫,同時在db_0和db_1庫中各自新建一個user表,db_0.user表和db_1.user表中各自只存原來的db.user表中的部分數(shù)據(jù)。
分庫分表:
? 將db庫拆分為db_0和db_1兩個庫,db_0中包含user_0、user_1兩個分表,db_1中包含user_2、user_3兩個分表。下圖演示了在分庫分表的情況下,數(shù)據(jù)是如何拆分的:假設(shè)db庫的user表中原來有4000W條數(shù)據(jù),現(xiàn)在將db庫拆分為2個分庫db_0和db_1,user表拆分為user_0、user_1、user_2、user_3四個分表,每個分表存儲1000W條數(shù)據(jù)。
分庫的好處:
? 降低單臺機器的負載壓力
分表的好處:
? 提高數(shù)據(jù)操作的效率。舉個例子說明,比如user表中現(xiàn)在有4000w條數(shù)據(jù),此時我們需要在這個表中增加(insert)一條新的數(shù)據(jù),insert完畢后,數(shù)據(jù)庫會針對這張表重新建立索引,4000w行數(shù)據(jù)建立索引的系統(tǒng)開銷還是不容忽視的。但是反過來,假如我們將這個表分成4 個table呢,從user_0一直到user_3,4000w行數(shù)據(jù)平均下來,每個子表里邊就只有1000W行數(shù)據(jù),這時候我們向一張 只有1000W行數(shù)據(jù)的table中insert數(shù)據(jù)后建立索引的時間就會下降,從而提高DB的運行時效率,提高了DB的并發(fā)量。當(dāng)然分表的好處還不知這些,還有諸如寫操作的鎖操作等,都會帶來很多顯然的好處。
分庫分表的挑戰(zhàn)主要體現(xiàn)在4個方面:基本的數(shù)據(jù)庫增刪改功能,分布式id,分布式事務(wù),動態(tài)擴容,下面逐一進行講述。
挑戰(zhàn)1:基本的數(shù)據(jù)庫增刪改功能
對于開發(fā)人員而言,雖然分庫分表的,但是其還是希望能和單庫單表那樣的去操作數(shù)據(jù)庫。例如我們要批量插入四條用戶記錄,并且希望根據(jù)用戶的id字段,確定這條記錄插入哪個庫的哪張表。例如1號記錄插入user_1表,2號記錄插入user_2表,3號記錄插入user_3表,4號記錄插入user_0表,以此類推。sql如下所示:
insert into user(id,name) values (1,”tianshouzhi”),(2,”huhuamin”), (3,”wanghanao”),(4,”luyang”)這樣的sql明顯是無法執(zhí)行的,因為我們已經(jīng)對庫和表進行了拆分,這種sql語法只能操作mysql的單個庫和單個表。所以必須將sql改成4條如下所示,然后分別到每個庫上去執(zhí)行。
insert into user_1(id,name) values (1,”tianshouzhi”)insert into user_2(id,name) values (2,”huhuamin”)insert into user_3(id,name) values (3,”wanghanao”)insert into user_0(id,name) values (4,”luyang”)具體流程可以用下圖進行描述:
解釋如下:
? sql解析:首先對sql進行解析,得到需要插入的四條記錄的id字段的值分別為1,2,3,4
? sql路由:sql路由包括庫路由和表路由。庫路由用于確定這條記錄應(yīng)該插入哪個庫,表路由用于確定這條記錄應(yīng)該插入哪個表。
? sql改寫:因為一條記錄只能插入到一個庫中,而上述批量插入的語法將會在 每個庫中都插入四條記錄,明顯是不合適的,因此需要對sql進行改寫,每個庫只插入一條記錄。
? sql執(zhí)行:一條sql經(jīng)過改寫后變成了多條sql,為了提升效率應(yīng)該并發(fā)的到不同的庫上去執(zhí)行,而不是按照順序逐一執(zhí)行
? 結(jié)果集合并:每個sql執(zhí)行之后,都會有一個執(zhí)行結(jié)果,我們需要對分庫分表的結(jié)果集進行合并,從而得到一個完整的結(jié)果。
挑戰(zhàn)2:分布式id
? 在分庫分表后,我們不能再使用mysql的自增主鍵。因為在插入記錄的時候,不同的庫生成的記錄的自增id可能會出現(xiàn)沖突。因此需要有一個全局的id生成器。目前分布式id有很多中方案,其中一個比較輕量級的方案是twitter的snowflake算法。
挑戰(zhàn)3:分布式事務(wù)
? 分布式事務(wù)是分庫分表繞不過去的一個坎,因此涉及到了同時更新多個數(shù)據(jù)庫。例如上面的批量插入記錄到四個不同的庫,如何保證要么同時成功,要么同時失敗。關(guān)于分布式事務(wù),mysql支持XA事務(wù),但是效率較低。柔性事務(wù)是目前比較主流的方案,柔性事務(wù)包括:最大努力通知型、可靠消息最終一致性方案以及TCC兩階段提交。但是無論XA事務(wù)還是柔性事務(wù),實現(xiàn)起來都是非常復(fù)雜的。
挑戰(zhàn)4:動態(tài)擴容
? 動態(tài)擴容指的是增加分庫分表的數(shù)量。例如原來的user表拆分到2個庫的四張表上。現(xiàn)在我們希望將分庫的數(shù)量變?yōu)?個,分表的數(shù)量變?yōu)?個。這種情況下一般要伴隨著數(shù)據(jù)遷移。例如在4張表的情況下,id為7的記錄,7%4=3,因此這條記錄位于user_3這張表上。但是現(xiàn)在分表的數(shù)量變?yōu)榱?個,而7%8=0,而user_0這張表上根本就沒有id=7的這條記錄,因此如果不進行數(shù)據(jù)遷移的話,就會出現(xiàn)記錄找不到的情況。本教程后面將會介紹一種在動態(tài)擴容時不需要進行數(shù)據(jù)遷移的方案。
4、總結(jié)
? 在上面我們已經(jīng)看到了,讀寫分離和分庫分表帶來的好處,但是也面臨了極大的挑戰(zhàn)。如果由業(yè)務(wù)開發(fā)人員來完成這些工作,難度比較大。因此就有一些公司專門來做一些數(shù)據(jù)庫中間件,對業(yè)務(wù)開發(fā)人員屏蔽底層的繁瑣細節(jié),開發(fā)人員使用了這些中間件后,不論是讀寫分離還是分庫分表,都可以像操作單庫單表那樣去操作。
總結(jié)
以上是生活随笔為你收集整理的数据库拆分过程及挑战的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HashMap和Hashtable
- 下一篇: Aurora的注册