分庫分表
讀寫分離分散了數(shù)據(jù)庫讀寫操作的壓力,但沒有分散存儲壓力,當數(shù)據(jù)量達到千萬甚至上億條的時候,單臺數(shù)據(jù)庫服務器的存儲能力會成為系統(tǒng)的瓶頸,主要體現(xiàn)在這幾個方面:
- 數(shù)據(jù)量太大,讀寫的性能會下降,即使有索引,索引也會變得很大,性能同樣會下降
- 數(shù)據(jù)文件會變得很大,數(shù)據(jù)庫備份和恢復需要耗費很長時間
- 數(shù)據(jù)文件越大,極端情況下丟失數(shù)據(jù)的風險越高(例如,機房火災導致數(shù)據(jù)庫主備機都發(fā)生故障)
基于上述原因,單個數(shù)據(jù)庫服務器存儲的數(shù)據(jù)量不能太大,需要控制在一定的范圍內(nèi)。為了滿足業(yè)務數(shù)據(jù)存儲的需求,就需要將存儲分散到多臺數(shù)據(jù)庫服務器上
- 常見的分散存儲的方法“分庫分表”,其中包括“分庫”和“分表”兩大類
業(yè)務分庫
-
業(yè)務分庫指的是按照業(yè)務模塊將數(shù)據(jù)分散到不同的數(shù)據(jù)庫服務器
- 一個簡單的電商網(wǎng)站,包括用戶、商品、訂單三個業(yè)務模塊
- 我們可以將用戶數(shù)據(jù)、商品數(shù)據(jù)、訂單數(shù)據(jù)分開放到三臺不同的數(shù)據(jù)庫服務器上,而不是將所有數(shù)據(jù)都放在一臺數(shù)據(jù)庫服務器上
業(yè)務分庫 雖然業(yè)務分庫能夠分散存儲和訪問壓力,但同時也帶來了新的問題:
- join 操作問題
- 業(yè)務分庫后,原本在同一個數(shù)據(jù)庫中的表分散到不同數(shù)據(jù)庫中,導致無法使用 SQL 的 join 查詢
- 例如:“查詢購買了化妝品的用戶中女性用戶的列表”這個功能,雖然訂單數(shù)據(jù)中有用戶的 ID 信息,但是用戶的性別數(shù)據(jù)在用戶數(shù)據(jù)庫中
- 如果在同一個庫中,簡單的 join 查詢就能完成
- 現(xiàn)在數(shù)據(jù)分散在兩個不同的數(shù)據(jù)庫中,無法做 join 查詢,只能采取先從訂單數(shù)據(jù)庫中查詢購買了化妝品的用戶 ID 列表,然后再到用戶數(shù)據(jù)庫中查詢這批用戶 ID 中的女性用戶列表,這樣實現(xiàn)就比簡單的 join 查詢要復雜一些
- 事務問題
- 原本在同一個數(shù)據(jù)庫中不同的表可以在同一個事務中修改,業(yè)務分庫后,表分散到不同的數(shù)據(jù)庫中,無法通過事務統(tǒng)一修改。雖然數(shù)據(jù)庫廠商提供了一些分布式事務的解決方案(例如,MySQL 的 XA),但性能實在太低,與高性能存儲的目標是相違背的
- 例如,用戶下訂單的時候需要扣商品庫存,如果訂單數(shù)據(jù)和商品數(shù)據(jù)在同一個數(shù)據(jù)庫中,我們可以使用事務來保證扣減商品庫存和生成訂單的操作要么都成功要么都失敗,但分庫后就無法使用數(shù)據(jù)庫事務了,需要業(yè)務程序自己來模擬實現(xiàn)事務的功能
- 例如,先扣商品庫存,扣成功后生成訂單,如果因為訂單數(shù)據(jù)庫異常導致生成訂單失敗,業(yè)務程序又需要將商品庫存加上;而如果因為業(yè)務程序自己異常導致生成訂單失敗,則商品庫存就無法恢復了,需要人工通過日志等方式來手工修復庫存異常
- 成本問題
- 業(yè)務分庫同時也帶來了成本的代價,本來 1 臺服務器搞定的事情,現(xiàn)在要 3 臺,如果考慮備份,那就是 2 臺變成了 6 臺
- 基于上述原因,對于小公司初創(chuàng)業(yè)務,并不建議一開始就這樣拆分,主要有幾個原因:
- 初創(chuàng)業(yè)務存在很大的不確定性,業(yè)務不一定能發(fā)展起來,業(yè)務開始的時候并沒有真正的存儲和訪問壓力,業(yè)務分庫并不能為業(yè)務帶來價值
- 業(yè)務分庫后,表之間的 join 查詢、數(shù)據(jù)庫事務無法簡單實現(xiàn)了
- 業(yè)務分庫后,因為不同的數(shù)據(jù)要讀寫不同的數(shù)據(jù)庫,代碼中需要增加根據(jù)數(shù)據(jù)類型映射到不同數(shù)據(jù)庫的邏輯,增加了工作量。而業(yè)務初創(chuàng)期間最重要的是快速實現(xiàn)、快速驗證,業(yè)務分庫會拖慢業(yè)務節(jié)奏
分表
將不同業(yè)務數(shù)據(jù)分散存儲到不同的數(shù)據(jù)庫服務器,能夠支撐百萬甚至千萬用戶規(guī)模的業(yè)務,但如果業(yè)務繼續(xù)發(fā)展,同一業(yè)務的單表數(shù)據(jù)也會達到單臺數(shù)據(jù)庫服務器的處理瓶頸。例如,淘寶的幾億用戶數(shù)據(jù),如果全部存放在一臺數(shù)據(jù)庫服務器的一張表中,肯定是無法滿足性能要求的,此時就需要對單表數(shù)據(jù)進行拆分
- 單表數(shù)據(jù)拆分有兩種方式:垂直分表和水平分表

分表的兩種方式
- 垂直拆分:示意圖中的垂直切分,會把表切分為兩個表,一個表包含 ID、name、age、sex 列,另外一個表包含 ID、nickname、description 列
- 水平拆分:示意圖中的水平切分,會把表分為兩個表,兩個表都包含 ID、name、age、sex、nickname、description 列,但是一個表包含的是 ID 從 1 到 999999 的行數(shù)據(jù),另一個表包含的是 ID 從 1000000 到 9999999 的行數(shù)據(jù)
- 架構設計過程中并不局限切分的次數(shù),可以切兩次,也可以切很多次,就像切蛋糕一樣,可以切很多刀
- 單表進行切分后,是否要將切分后的多個表分散在不同的數(shù)據(jù)庫服務器中,可以根據(jù)實際的切分效果來確定,并不強制要求單表切分為多表后一定要分散到不同數(shù)據(jù)庫中
- 單表切分為多表后,新的表即使在同一個數(shù)據(jù)庫服務器中,也可能帶來可觀的性能提升,如果性能能夠滿足業(yè)務要求,是可以不拆分到多臺數(shù)據(jù)庫服務器的,畢竟我們在上面業(yè)務分庫的內(nèi)容看到業(yè)務分庫也會引入很多復雜性的問題
- 如果單表拆分為多表后,單臺服務器依然無法滿足性能要求,那就不得不再次進行業(yè)務分庫的設計了
- 分表能夠有效地分散存儲壓力和帶來性能提升,但和分庫一樣,也會引入各種復雜性:
- 垂直分表
- 垂直分表適合將表中某些不常用且占了大量空間的列拆分出去
- 垂直分表引入的復雜性主要體現(xiàn)在表操作的數(shù)量要增加
- 水平分表
- 水平分表適合表行數(shù)特別大的表,有的公司要求單表行數(shù)超過 5000 萬就必須進行分表,這個數(shù)字可以作為參考,但并不是絕對標準,關鍵還是要看表的訪問性能
- 對于一些比較復雜的表,可能超過 1000 萬就要分表了
- 而對于一些簡單的表,即使存儲數(shù)據(jù)超過 1 億行,也可以不分表
- 水平分表相比垂直分表,會引入更多的復雜性,主要表現(xiàn)在下面幾個方面:
- 路由:
- 水平分表后,某條數(shù)據(jù)具體屬于哪個切分后的子表,需要增加路由算法進行計算,這個算法會引入一定的復雜性
- 常見的路由算法有:
- 范圍路由:選取有序的數(shù)據(jù)列(例如,整形、時間戳等)作為路由的條件,不同分段分散到不同的數(shù)據(jù)庫表中。以最常見的用戶 ID 為例,路由算法可以按照 1000000 的范圍大小進行分段,1 ~ 999999 放到數(shù)據(jù)庫 1 的表中,1000000 ~ 1999999 放到數(shù)據(jù)庫 2 的表中,以此類推
- 范圍路由設計的復雜點主要體現(xiàn)在分段大小的選取上,分段太小會導致切分后子表數(shù)量過多,增加維護復雜度;分段太大可能會導致單表依然存在性能問題,一般建議分段大小在 100 萬至 2000 萬之間,具體需要根據(jù)業(yè)務選取合適的分段大小
- 范圍路由的優(yōu)點是可以隨著數(shù)據(jù)的增加平滑地擴充新的表。例如,現(xiàn)在的用戶是 100 萬,如果增加到 1000 萬,只需要增加新的表就可以了,原有的數(shù)據(jù)不需要動
- 范圍路由的一個比較隱含的缺點是分布不均勻,假如按照 1000 萬來進行分表,有可能某個分段實際存儲的數(shù)據(jù)量只有 1000 條,而另外一個分段實際存儲的數(shù)據(jù)量有 900 萬條
- Hash 路由:選取某個列(或者某幾個列組合也可以)的值進行 Hash 運算,然后根據(jù) Hash 結果分散到不同的數(shù)據(jù)庫表中。同樣以用戶 ID 為例,假如我們一開始就規(guī)劃了 10 個數(shù)據(jù)庫表,路由算法可以簡單地用 user_id % 10 的值來表示數(shù)據(jù)所屬的數(shù)據(jù)庫表編號,ID 為 985 的用戶放到編號為 5 的子表中,ID 為 10086 的用戶放到編號為 6 的字表中
- Hash 路由設計的復雜點主要體現(xiàn)在初始表數(shù)量的選取上,表數(shù)量太多維護比較麻煩,表數(shù)量太少又可能導致單表性能存在問題。而用了 Hash 路由后,增加子表數(shù)量是非常麻煩的,所有數(shù)據(jù)都要重分布
- Hash 路由的優(yōu)缺點和范圍路由基本相反,Hash 路由的優(yōu)點是表分布比較均勻,缺點是擴充新的表很麻煩,所有數(shù)據(jù)都要重分布
- 配置路由:配置路由就是路由表,用一張獨立的表來記錄路由信息。同樣以用戶 ID 為例,我們新增一張 user_router 表,這個表包含 user_id 和 table_id 兩列,根據(jù) user_id 就可以查詢對應的 table_id
- 配置路由設計簡單,使用起來非常靈活,尤其是在擴充表的時候,只需要遷移指定的數(shù)據(jù),然后修改路由表就可以了
- 配置路由的缺點就是必須多查詢一次,會影響整體性能;而且路由表本身如果太大(例如,幾億條數(shù)據(jù)),性能同樣可能成為瓶頸,如果我們再次將路由表分庫分表,則又面臨一個死循環(huán)式的路由算法選擇問題
- 范圍路由:選取有序的數(shù)據(jù)列(例如,整形、時間戳等)作為路由的條件,不同分段分散到不同的數(shù)據(jù)庫表中。以最常見的用戶 ID 為例,路由算法可以按照 1000000 的范圍大小進行分段,1 ~ 999999 放到數(shù)據(jù)庫 1 的表中,1000000 ~ 1999999 放到數(shù)據(jù)庫 2 的表中,以此類推
- join 操作:
- 水平分表后,數(shù)據(jù)分散在多個表中,如果需要與其他表進行 join 查詢,需要在業(yè)務代碼或者數(shù)據(jù)庫中間件中進行多次 join 查詢,然后將結果合并
- count() 操作
- 水平分表后,雖然物理上數(shù)據(jù)分散到多個表中,但某些業(yè)務邏輯上還是會將這些表當作一個表來處理。例如,獲取記錄總數(shù)用于分頁或者展示,水平分表前用一個 count() 就能完成的操作,在分表后就沒那么簡單了。常見的處理方式有下面兩種:
- count() 相加:具體做法是在業(yè)務代碼或者數(shù)據(jù)庫中間件中對每個表進行 count() 操作,然后將結果相加。這種方式實現(xiàn)簡單,缺點就是性能比較低。例如,水平分表后切分為 20 張表,則要進行 20 次 count(*) 操作,如果串行的話,可能需要幾秒鐘才能得到結果
- 記錄數(shù)表:具體做法是新建一張表,假如表名為“記錄數(shù)表”,包含 table_name、row_count 兩個字段,每次插入或者刪除子表數(shù)據(jù)成功后,都更新“記錄數(shù)表”
- 這種方式獲取表記錄數(shù)的性能要大大優(yōu)于 count() 相加的方式,因為只需要一次簡單查詢就可以獲取數(shù)據(jù)
- 缺點是復雜度增加不少,對子表的操作要同步操作“記錄數(shù)表”,如果有一個業(yè)務邏輯遺漏了,數(shù)據(jù)就會不一致;且針對“記錄數(shù)表”的操作和針對子表的操作無法放在同一事務中進行處理,異常的情況下會出現(xiàn)操作子表成功了而操作記錄數(shù)表失敗,同樣會導致數(shù)據(jù)不一致
- 記錄數(shù)表的方式也增加了數(shù)據(jù)庫的寫壓力,因為每次針對子表的 insert 和 delete 操作都要 update 記錄數(shù)表,所以對于一些不要求記錄數(shù)實時保持精確的業(yè)務,也可以通過后臺定時更新記錄數(shù)表。定時更新實際上就是“count() 相加”和“記錄數(shù)表”的結合,即定時通過 count() 相加計算表的記錄數(shù),然后更新記錄數(shù)表中的數(shù)據(jù)
- order by 操作
- 水平分表后,數(shù)據(jù)分散到多個子表中,排序操作無法在數(shù)據(jù)庫中完成,只能由業(yè)務代碼或者數(shù)據(jù)庫中間件分別查詢每個子表中的數(shù)據(jù),然后匯總進行排序
實現(xiàn)方法
- 和數(shù)據(jù)庫讀寫分離類似,分庫分表具體的實現(xiàn)方式也是“程序代碼封裝”和“中間件封裝”,但實現(xiàn)會更復雜
- 讀寫分離實現(xiàn)時只要識別 SQL 操作是讀操作還是寫操作,通過簡單的判斷 SELECT、UPDATE、INSERT、DELETE 幾個關鍵字就可以做到,而分庫分表的實現(xiàn)除了要判斷操作類型外,還要判斷 SQL 中具體需要操作的表、操作函數(shù)(例如 count 函數(shù))、order by、group by 操作等
- 根據(jù)不同的操作進行不同的處理。例如 order by 操作,需要先從多個庫查詢到各個庫的數(shù)據(jù),然后再重新 order by 才能得到最終的結果
小結
本文講解了高性能數(shù)據(jù)庫集群的分庫分表架構,包括業(yè)務分庫產(chǎn)生的問題和分表的兩種方式及其帶來的復雜度,希望對你有所幫助
