數據庫拆分過程及挑戰

互聯網當下的數據庫拆分過程基本遵循的順序是:垂直拆分、讀寫分離、分庫分表(水平拆分)。每個拆分過程都能解決業務上的一些問題,但同時也面臨了一些挑戰。

1 垂直拆分

對於一個剛上線的互聯網項目來說,由於前期活躍用戶數量並不多,併發量也相對較小,所以此時企業一般都會選擇將所有數據存放在一個數據庫 中進行訪問操作。

舉例來說,對於一個電商系統,其用戶模塊和產品模塊的表剛開始都是位於一個db_eshop庫中。

Image.png

其中:user表和user_account表屬於用戶模塊,product_category表和product表屬於產品模塊

剛開始,可能公司的技術團隊規模比較小,因此整個技術團隊共同維護db_eshop庫。隨着公司業務的發展,技術團隊人員也得到了擴張,劃分爲不同的技術小組,不同的小組負責不同的業務模塊。例如A小組負責用戶模塊,B小組負責產品模塊。此時數據庫也迎來了第一次拆分:垂直拆分。

這裏的垂直拆分,指的是將一個包含了很多表的數據庫,根據表的功能的不同,拆分爲多個小的數據庫,每個庫包含部分表。下圖演示將上面提到的db_eshop庫,拆分爲db_user庫和db_product庫。

Image.png

關於垂直拆分,還有另一種說法,將一個包含了很多字段的大表拆分爲多個小表,每個表包含部分字段。而筆者認爲,根據表功能的不同的對數據庫進行拆分,這種情況更加常見。

2 讀寫分離

隨着後續的市場推廣力度不斷加強,用戶數量和併發量不斷上升。這時如果僅靠一個數據庫來支撐所有訪問壓力,幾乎是在 自尋死路 。以產品庫爲例,可能庫中包含了幾萬種商品,並且每天新增幾十種,而產品庫每天的訪問了可能有幾億甚至幾十億次。數據庫讀的壓力太大,單臺mysql實例扛不住,此時大部分 Mysql DBA 就會將數據庫設置成 讀寫分離狀態 ,也就是一個 Master 節點(主庫)對應多個 Salve 節點(從庫)。可以將slave節點的數據理解爲master節點數據的全量備份。

Image.png

master節點只有一個且可讀可寫,slave節點有多個且只可以讀。新增產品時,應用將數據寫入master主庫,主庫將數據同步給多個slave從庫。當查詢產品時,應用選擇某個salve節點讀取數據。

讀寫分離的優點:

    這樣通過配置多個slave節點,可以有效的避免過大的訪問量對單個庫造成的壓力。

讀寫分離的挑戰:

1、對於DBA而言,需要配置數據庫主從同步

     關於如何配置數據庫的主從同步,這個目前方案已經很成熟。以mysql爲例:

     可以參考官方文檔:https://dev.mysql.com/doc/refman/5.7/en/replication.html,

     筆者也寫了一篇文章介紹如何通過mysql_multi的方式配置主從同步:http://www.tianshouzhi.com/api/tutorials/mysql

2、對於開發人員而言,必須要對sql類型進行判斷,如果是select等讀請求,就走從庫,如果是insert、update、delete等寫請求,就走主庫。此外還有一些其他的問題要考慮:

  • 主從數據同步延遲問題:因爲數據是從master節點通過網絡同步給多個slave節點,因此必然存在延遲。因此有可能出現我們在master節點中已經插入了數據,但是從slave節點卻讀取不到的問題。對於一些強一致性的業務場景,要求插入後必須能讀取到,因此對於這種情況,我們需要提供一種方式,讓讀請求也可以走主庫,而主庫上的數據必然是最新的。

  • 事務問題:如果一個事務中同時包含了讀請求(如select)和寫請求(如insert),如果讀請求走從庫,寫請求走主庫,由於跨了多個庫,那麼jdbc本地事務已經無法控制,屬於分佈式事務的範疇。而分佈式事務非常複雜且效率較低。因此對於讀寫分離,目前主流的做法是,事務中的所有sql統一都走主庫,由於只涉及到一個庫,jdbc本地事務就可以搞定。

  • 高可用的考慮:例如master配置了多個slave節點,如果其中某個slave節點掛了,那麼之後的讀請求,我們應用將其轉發到正常工作的slave節點上。另外,如果新增了slave節點,應用也應該感知到,可以將讀請求轉發到新的slave節點上。

3 分庫分表

        經過垂直分區後的 Master/Salve 模式完全可以承受住難以想象的高併發訪問操作,但是否可以永遠 高枕無憂 了?答案是否定的,一旦業務表中的數據量大了,從維護和性能角度來看,無論是任何的 CRUD 操作,對於數據庫而言都是一件極其耗費資源的事情。即便設置了索引, 仍然無法掩蓋因爲數據量過大從而導致的數據庫性能下降的事實 ,因此這個時候 Mysql DBA 或許就該對數據庫進行 水平分區 (sharding,即分庫分表 )。經過水平分區設置後的業務表,必然能夠將原本一張表維護的海量數據分配給 N 個子表進行存儲和維護。

水平分表從具體實現上又可以分爲3種:只分表、只分庫、分庫分表,下圖展示了這三種情況:

Image.png

 

只分表:

        將db庫中的user表拆分爲2個分表,user_0和user_1,這兩個表還位於同一個庫中。  適用場景:如果庫中的多個表中只有某張表或者少數表數據量過大,那麼只需要針對這些表進行拆分,其他表保持不變。

只分庫:

        將db庫拆分爲db_0和db_1兩個庫,同時在db_0和db_1庫中各自新建一個user表,db_0.user表和db_1.user表中各自只存原來的db.user表中的部分數據。

分庫分表:

        將db庫拆分爲db_0和db_1兩個庫,db_0中包含user_0、user_1兩個分表,db_1中包含user_2、user_3兩個分表。下圖演示了在分庫分表的情況下,數據是如何拆分的:假設db庫的user表中原來有4000W條數據,現在將db庫拆分爲2個分庫db_0和db_1,user表拆分爲user_0、user_1、user_2、user_3四個分表,每個分表存儲1000W條數據。

Image.png

分庫的好處:

    降低單臺機器的負載壓力

分表的好處:

    提高數據操作的效率。舉個例子說明,比如user表中現在有4000w條數據,此時我們需要在這個表中增加(insert)一條新的數據,insert完畢後,數據庫會針對這張表重新建立索引,4000w行數據建立索引的系統開銷還是不容忽視的。但是反過來,假如我們將這個表分成4 個table呢,從user_0一直到user_3,4000w行數據平均下來,每個子表裏邊就只有1000W行數據,這時候我們向一張 只有1000W行數據的table中insert數據後建立索引的時間就會下降,從而提高DB的運行時效率,提高了DB的併發量。當然分表的好處還不知這些,還有諸如寫操作的鎖操作等,都會帶來很多顯然的好處。

 

分庫分表的挑戰主要體現在4個方面:基本的數據庫增刪改功能,分佈式id,分佈式事務,動態擴容,下面逐一進行講述。 

挑戰1:基本的數據庫增刪改功能   

對於開發人員而言,雖然分庫分表的,但是其還是希望能和單庫單表那樣的去操作數據庫。例如我們要批量插入四條用戶記錄,並且希望根據用戶的id字段,確定這條記錄插入哪個庫的哪張表。例如1號記錄插入user_1表,2號記錄插入user_2表,3號記錄插入user_3表,4號記錄插入user_0表,以此類推。sql如下所示:


 
  1. insert into user(id,name) values (1,”tianshouzhi”),(2,”huhuamin”), (3,”wanghanao”),(4,”luyang”)

 這樣的sql明顯是無法執行的,因爲我們已經對庫和表進行了拆分,這種sql語法只能操作mysql的單個庫和單個表。所以必須將sql改成4條如下所示,然後分別到每個庫上去執行。


 
  1. insert into user_1(id,name) values (1,”tianshouzhi”)
  2. insert into user_2(id,name) values (2,”huhuamin”)
  3. insert into user_3(id,name) values (3,”wanghanao”)
  4. insert into user_0(id,name) values  (4,”luyang”)

具體流程可以用下圖進行描述:

Image.png

解釋如下:

    sql解析:首先對sql進行解析,得到需要插入的四條記錄的id字段的值分別爲1,2,3,4

    sql路由:sql路由包括庫路由和表路由。庫路由用於確定這條記錄應該插入哪個庫,表路由用於確定這條記錄應該插入哪個表。

    sql改寫:因爲一條記錄只能插入到一個庫中,而上述批量插入的語法將會在 每個庫中都插入四條記錄,明顯是不合適的,因此需要對sql進行改寫,每個庫只插入一條記錄。

    sql執行:一條sql經過改寫後變成了多條sql,爲了提升效率應該併發的到不同的庫上去執行,而不是按照順序逐一執行

    結果集合並:每個sql執行之後,都會有一個執行結果,我們需要對分庫分表的結果集進行合併,從而得到一個完整的結果。

挑戰2:分佈式id

    在分庫分表後,我們不能再使用mysql的自增主鍵。因爲在插入記錄的時候,不同的庫生成的記錄的自增id可能會出現衝突。因此需要有一個全局的id生成器。目前分佈式id有很多中方案,其中一個比較輕量級的方案是twitter的snowflake算法。

挑戰3:分佈式事務

    分佈式事務是分庫分表繞不過去的一個坎,因此涉及到了同時更新多個數據庫。例如上面的批量插入記錄到四個不同的庫,如何保證要麼同時成功,要麼同時失敗。關於分佈式事務,mysql支持XA事務,但是效率較低。柔性事務是目前比較主流的方案,柔性事務包括:最大努力通知型、可靠消息最終一致性方案以及TCC兩階段提交。但是無論XA事務還是柔性事務,實現起來都是非常複雜的。

挑戰4:動態擴容

    動態擴容指的是增加分庫分表的數量。例如原來的user表拆分到2個庫的四張表上。現在我們希望將分庫的數量變爲4個,分表的數量變爲8個。這種情況下一般要伴隨着數據遷移。例如在4張表的情況下,id爲7的記錄,7%4=3,因此這條記錄位於user_3這張表上。但是現在分表的數量變爲了8個,而7%8=0,而user_0這張表上根本就沒有id=7的這條記錄,因此如果不進行數據遷移的話,就會出現記錄找不到的情況。本教程後面將會介紹一種在動態擴容時不需要進行數據遷移的方案。

4、總結

    在上面我們已經看到了,讀寫分離和分庫分錶帶來的好處,但是也面臨了極大的挑戰。如果由業務開發人員來完成這些工作,難度比較大。因此就有一些公司專門來做一些數據庫中間件,對業務開發人員屏蔽底層的繁瑣細節,開發人員使用了這些中間件後,不論是讀寫分離還是分庫分表,都可以像操作單庫單表那樣去操作。

下面我們將介紹 主流的數據庫中間件設計方案和實現。 

數據庫中間件設計方案

 

        數據庫中間件的主要作用是嚮應用程序開發人員屏蔽讀寫分離和分庫分表面臨的挑戰,並隱藏底層實現細節,使得開發人員可以像操作單庫單表那樣去操作數據。在介紹分庫分表的主流設計方案前,我們首先回顧一下在單個庫的情況下,應用的架構,可以用下圖進行描述:

        Image.png

可以看到在操作單庫單表的情況下,我們是直接在應用中通過數據源(c3p0、druid、dbcp等)與數據庫建立連接,進行讀寫操作。而對於讀寫分離和分庫分表,應用都要操作多個數據庫實例,在這種情況下,我們就需要使用到數據庫中間件。

1 主流的數據庫中間件設計方案

典型的數據庫中間件設計方案有2種:服務端代理(代理數據庫)、客戶端代理(代理數據源)。下圖演示了這兩種方案的架構:

Image.png

 

可以看到不論是代理數據庫還是代理數據源,底層都操作了多個數據庫實例。不同的是:

  • 在數據庫代理中:

我們獨立部署一個代理服務,這個代理服務背後管理多個數據庫實例。而在應用中,我們通過一個普通的數據源(c3p0、druid、dbcp等)與代理服務器建立連接,所有的sql操作語句都是發送給這個代理,由這個代理去操作底層數據庫,得到結果並返回給應用。在這種方案下,分庫分表和讀寫分離的邏輯對開發人員是完全透明的。

  • 在數據源代理中:

應用程序需要使用一個特定的數據源,其作用是代理,內部管理了多個普通的數據源(c3p0、druid、dbcp等),每個普通數據源各自與不同的庫建立連接。應用程序產生的sql交給數據源代理進行處理,數據源內部對sql進行必要的操作,如sql改寫等,然後交給各個普通的數據源去執行,將得到的結果進行合併,返回給應用。數據源代理通常也實現了JDBC規範定義的API,因此能夠直接與orm框架整合。

2 主流的數據庫中間件實現

無論是代理數據庫,還是代理數據源,二者的作用都是類似的。以下列出了這兩種方案目前已有的實現以及各自的優缺點:

Image.png

數據庫代理

    目前的實現方案有:阿里巴巴開源的cobar,mycat團隊在cobar基礎上開發的mycat,mysql官方提供的mysql-proxy,奇虎360在mysql-proxy基礎開發的atlas。目前除了mycat,其他幾個項目基本已經沒有維護。

    優點:多語言支持。也就是說,不論你用的php、java或是其他語言,都可以支持。原因在於數據庫代理本身就實現了mysql的通信協議,你可以就將其看成一個mysql 服務器。mysql官方團隊爲不同語言提供了不同的客戶端卻動,如java語言的mysql-connector-java,python語言的mysql-connector-python等等。因此不同語言的開發者都可以使用mysql官方提供的對應的驅動來與這個代理服務器建通信。

     缺點:實現複雜。因爲代理服務器需要實現mysql服務端的通信協議,因此實現難度較大。

數據源代理

    目前的實現方案有:阿里巴巴開源的tddl,大衆點評開源的zebra,噹噹網開源的sharding-jdbc。需要注意的是tddl的開源版本只有讀寫分離功能,沒有分庫分表,且開源版本已經不再維護。大衆點評的zebra開源版本代碼已經很久更新,基本上處於停滯的狀態。噹噹網的sharding-jdbc目前算是做的比較好的,代碼時有更新,文檔資料比較全。

     優點:更加輕量,可以與任何orm框架整合。這種方案不需要實現mysql的通信協議,因爲底層管理的普通數據源,可以直接通過mysql-connector-java驅動與mysql服務器進行通信,因此實現相對簡單。

     缺點:僅支持某一種語言。例如tddl、zebra、sharding-jdbc都是使用java語言開發,因此對於使用其他語言的用戶,就無法使用這些中間件。版本升級困難,因爲應用使用數據源代理就是引入一個jar包的依賴,在有多個應用都對某個版本的jar包產生依賴時,一旦這個版本有bug,所有的應用都需要升級。而數據庫代理升級則相對容易,因爲服務是單獨部署的,只要升級這個代理服務器,所有連接到這個代理的應用自然也就相當於都升級了。

ORM框架代理:

        目前有hibernate提供的hibernate-shards,也可以通過mybatis插件的方式編寫。相對於前面兩種方案,這種方案可以說是隻有缺點,沒有優點。  

只有中國公司貢獻開源中間件,而且停止更新了, 原因是,現在隨着數據庫領域的發展,MYSQL, ORACLE 等都將中間件功能集成了,大家都在搞分佈式數據庫了,分佈式數據庫的容量上限遠大於傳統的關係型數據庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章