為什么使用分庫分表?
如下內容,引用自 Sharding Sphere 的文檔,寫的很大氣。
傳統的將數據集中存儲至單一數據節點的解決方案,在性能、可用性和運維成本這三方面已經難於滿足互聯網的海量數據場景。
1)性能
從性能方面來說,由於關系型數據庫大多采用 B+ 樹類型的索引,在數據量超過閾值的情況下,索引深度的增加也將使得磁盤訪問的 IO 次數增加,進而導致查詢性能的下降。
同時,高並發訪問請求也使得集中式數據庫成為系統的最大瓶頸。
2)可用性
從可用性的方面來講,服務化的無狀態型,能夠達到較小成本的隨意擴容,這必然導致系統的最終壓力都落在數據庫之上。而單一的數據節點,或者簡單的主從架構,已經越來越難以承擔。數據庫的可用性,已成為整個系統的關鍵。
3)運維成本
從運維成本方面考慮,當一個數據庫實例中的數據達到閾值以上,對於 DBA 的運維壓力就會增大。數據備份和恢復的時間成本都將隨着數據量的大小而愈發不可控。一般來講,單一數據庫實例的數據的閾值在 1TB 之內,是比較合理的范圍。
那么為什么不選擇 NoSQL 呢?
在傳統的關系型數據庫無法滿足互聯網場景需要的情況下,將數據存儲至原生支持分布式的 NoSQL 的嘗試越來越多。 但 NoSQL 對 SQL 的不兼容性以及生態圈的不完善,使得它們在與關系型數據庫的博弈中始終無法完成致命一擊,而關系型數據庫的地位卻依然不可撼動。
什么是分庫分表?
數據分片,指按照某個維度將存放在單一數據庫中的數據分散地存放至多個數據庫或表中以達到提升性能瓶頸以及可用性的效果。數據分片的有效手段是對關系型數據庫進行分庫和分表。
- 分庫和分表均可以有效的避免由數據量超過可承受閾值而產生的查詢瓶頸。除此之外,分庫還能夠用於有效的分散對數據庫單點的訪問量。
- 分表雖然無法緩解數據庫壓力,但卻能夠提供盡量將分布式事務轉化為本地事務的可能,一旦涉及到跨庫的更新操作,分布式事務往往會使問題變得復雜。
- 使用多主多從的分片方式,可以有效的避免數據單點,從而提升數據架構的可用性。
通過分庫和分表進行數據的拆分來使得各個表的數據量保持在閾值以下,以及對流量進行疏導應對高訪問量,是應對高並發和海量數據系統的有效手段。數據分片的拆分方式又分為垂直分片和水平分片。
垂直分片
按照業務拆分的方式稱為垂直分片,又稱為縱向拆分,它的核心理念是專庫專用。 在拆分之前,一個數據庫由多個數據表構成,每個表對應着不同的業務。而拆分之后,則是按照業務將表進行歸類,分布到不同的數據庫中,從而將壓力分散至不同的數據庫。
下圖展示了根據業務需要,將用戶表和訂單表垂直分片到不同的數據庫的方案:
垂直分片往往需要對架構和設計進行調整。通常來講,是來不及應對互聯網業務需求快速變化的;而且,它也並無法真正的解決單點瓶頸。 垂直拆分可以緩解數據量和訪問量帶來的問題,但無法根治。如果垂直拆分之后,表中的數據量依然超過單節點所能承載的閾值,則需要水平分片來進一步處理。
垂直拆分的優點:
- 庫表職責單一,復雜度降低,易於維護。
- 單庫或單表壓力降低。 相互之間的影響也會降低。
垂直拆分的缺點:
- 部分表關聯無法在數據庫級別完成,需要在程序中完成。
- 單表大數據量仍然存在性能瓶頸。
- 單表或單庫高熱點訪問依舊對 DB 壓力非常大。
- 事務處理相對更為復雜,需要分布式事務的介入。
- 拆分達到一定程度之后,擴展性會遇到限制。
水平分片
水平分片又稱為橫向拆分。 相對於垂直分片,它不再將數據根據業務邏輯分類,而是通過某個字段(或某幾個字段),根據某種規則將數據分散至多個庫或表中,每個分片僅包含數據的一部分。
例如:根據主鍵分片,偶數主鍵的記錄放入 0 庫(或表),奇數主鍵的記錄放入 1 庫(或表),如下圖所示:
水平分片從理論上突破了單機數據量處理的瓶頸,並且擴展相對自由,是分庫分表的標准解決方案。
水平拆分的優點:
- 解決單表單庫大數據量和高熱點訪問性能遇到瓶頸的問題。
- 應用程序端整體架構改動相對較少。
- 事務處理相對簡單。
- 只要切分規則能夠定義好,基本上較難遇到擴展性限制。
水平拆分缺點:
- 拆分規則相對更復雜,很難抽象出一個能夠滿足整個數據庫的切分規則。
- 后期數據的維護難度有所增加,人為手工定位數據更困難。
- 產品邏輯將變復雜。比如按年來進行歷史數據歸檔拆分,這個時候在頁面設計上就需要約束用戶必須要先選擇年,然后才能進行查詢。
總結
- 數據表垂直拆分:單表復雜度。
- 數據庫垂直拆分:功能拆分。
- 水平拆分
- 分表:解決單表大數據量問題。
- 分庫:為了解決單庫性能問題。
用了分庫分表之后,有哪些常見問題?
雖然數據分片解決了性能、可用性以及單點備份恢復等問題,但分布式的架構在獲得了收益的同時,也引入了新的問題。
- 面對如此散亂的分庫分表之后的數據,應用開發工程師和數據庫管理員對數據庫的操作變得異常繁重就是其中的重要挑戰之一。他們需要知道數據需要從哪個具體的數據庫的分表中獲取。
- 另一個挑戰則是,能夠正確的運行在單節點數據庫中的 SQL ,在分片之后的數據庫中並不一定能夠正確運行。
- 例如,分表導致表名稱的修改,或者分頁、排序、聚合分組等操作的不正確處理。
- 例如,跨節點 join 的問題。
-
跨庫事務也是分布式的數據庫集群要面對的棘手事情。
-
合理采用分表,可以在降低單表數據量的情況下,盡量使用本地事務,善於使用同庫不同表可有效避免分布式事務帶來的麻煩。
要達到這個效果,需要盡量把同一組數據放到同一組 DB 服務器上。
例如說,將同一個用戶的訂單主表,和訂單明細表放到同一個庫,那么在創建訂單時,還是可以使用相同本地事務。
-
在不能避免跨庫事務的場景,有些業務仍然需要保持事務的一致性。而基於 XA 的分布式事務由於在並發度高的場景中性能無法滿足需要,並未被互聯網巨頭大規模使用,他們大多采用最終一致性的柔性事務代替強一致事務。
-
- 分布式全局唯一 ID 。
- 在單庫單表的情況下,直接使用數據庫自增特性來生成主鍵ID,這樣確實比較簡單。
- 在分庫分表的環境中,數據分布在不同的分表上,不能再借助數據庫自增長特性。需要使用全局唯一 ID,例如 UUID、GUID等 。
關於這塊,也可以看看 《分庫分表》 文章。
了解和使用過哪些分庫分表中間件?
在將數據庫進行分庫分表之后,我們一般會引入分庫分表的中間件,使之能夠達到如下目標。
盡量透明化分庫分表所帶來的影響,讓使用方盡量像使用一個數據庫一樣使用水平分片之后的數據庫集群,這是分庫分表的主要設計目標。
分庫分表的實現方式?
目前,市面上提供的分庫分表的中間件,主要有兩種實現方式:
- Client 模式
- Proxy 模式
分庫分表中間件?
比較常見的包括:
- Cobar
- MyCAT
- Atlas
- TDDL
- Sharding Sphere
1)Cobar
阿里 b2b 團隊開發和開源的,屬於 Proxy 層方案。
早些年還可以用,但是最近幾年都沒更新了,基本沒啥人用,差不多算是被拋棄的狀態吧。而且不支持讀寫分離、存儲過程、跨庫 join 和分頁等操作。
2)MyCAT
基於 Cobar 改造的,屬於 Proxy 層方案,支持的功能非常完善,而且目前應該是非常火的而且不斷流行的數據庫中間件,社區很活躍,也有一些公司開始在用了。但是確實相比於 Sharding Sphere 來說,年輕一些,經歷的錘煉少一些。
3)Atlas
360 開源的,屬於 Proxy 層方案,以前是有一些公司在用的,但是確實有一個很大的問題就是社區最新的維護都在 5 年前了。所以,現在用的公司基本也很少了。
4)TDDL
淘寶團隊開發的,屬於 client 層方案。支持基本的 crud 語法和讀寫分離,但不支持 join、多表查詢等語法。目前使用的也不多,因為還依賴淘寶的 diamond 配置管理系統。
5)Sharding Sphere
Sharding Sphere ,可能是目前最好的開源的分庫分表解決方案,目前已經進入 Apache 孵化。
Sharding Sphere 提供三種模式:
關於每一種模式的介紹,可以看看 《ShardingSphere > 概覽》
- Sharding-JDBC
- Sharding-Proxy
- Sharding-Sidecar 計划開發中。
其中,Sharding-JDBC 屬於 client 層方案,被大量互聯網公司所采用。例如,當當、京東金融、中國移動等等。
如何選擇?
綜上,現在其實建議考量的,就是 Sharding Sphere ,這個可以滿足我們的訴求。
Sharding Sphere 的 Sharding-JDBC 方案,這種 Client 層方案的優點在於不用部署,運維成本低,不需要代理層的二次轉發請求,性能很高,但是如果遇到升級啥的需要各個系統都重新升級版本再發布,各個系統都需要耦合 sharding-jdbc 的依賴。
Sharding Sphere 的 Sharding-Proxy 方案,這種 Proxy 層方案,可以解決我們平時查詢數據庫的需求。我們只需要連接一個 Sharding-Proxy ,就可以查詢分庫分表中的數據。另外,如果我們有跨語言的需求,例如 PHP、GO 等,也可以使用它。
如何遷移到分庫分表?
一般來說,會有三種方式:
- 1、停止部署法。
- 2、雙寫部署法,基於業務層。
- 3、雙寫部署法,基於 binlog 。
具體的詳細方案,可以看看如下兩篇文章:
另外,這是另外一個比較相對詳細的【雙寫部署法,基於業務層】的過程:
- 雙寫 ,老庫為主。讀操作還是讀老庫老表,寫操作是雙寫到新老表。
- 歷史數據遷移 dts + 新數據對賬校驗(job) + 歷史數據校驗。
- 切讀:讀寫以新表為主,新表成功就成功了。
- 觀察幾天,下掉寫老庫操作。
另外,飛哥的 《不停機分庫分表遷移》 文章,也非常推薦看看。
如何設計可以動態擴容縮容的分庫分表方案?
可以參看 《如何設計可以動態擴容縮容的分庫分表方案?》 文章。簡單的結論是:
- 提前考慮好容量的規划,避免擴容的情況。
- 如果真的需要擴容,走上述的 「如何遷移到分庫分表?」 提到的方案。
什么是分布式主鍵?怎么實現?
分布式主鍵的實現方案有很多,可以看看 《談談 ID》 的總結。
一般來說,目前采用 SnowFlake 的居多,可以看看 《Sharding-JDBC 源碼分析 —— 分布式主鍵》 的源碼的具體實現,比較簡單。
分片鍵的選擇?
分庫分表后,分片鍵的選擇非常重要。一般來說是這樣的:
- 信息表,使用 id 進行分片。例如說,文章、商品信息等等。
- 業務表,使用 user_id 進行分片。例如說,訂單表、支付表等等。
- 日志表,使用 create_time 進行分片。例如說,訪問日志、登陸日志等等。
分片算法的選擇?
選擇好分片鍵之后,還需要考慮分片算法。一般來說,有如下兩種:
- 取余分片算法。例如說,有四個庫,那么 user_id 為 10 時,分到第
10 % 4 = 2
個庫。- 當然,如果分片鍵是字符串,則需要先進行 hash 的方式,轉換成整形,這樣才可以取余。
- 當然,如果分片鍵是整數,也可以使用 hash 的方式。
- 范圍算法。
- 例如說,時間范圍。
上述兩種算法,各有優缺點。
- 對於取余來說:
- 好處,可以平均分配每個庫的數據量和請求壓力。
- 壞處,在於說擴容起來比較麻煩,會有一個數據遷移的過程,之前的數據需要重新計算 hash 值重新分配到不同的庫或表。
- 對於 range 來說:
- 好處,擴容的時候很簡單,因為你只要預備好,給每個月都准備一個庫就可以了,到了一個新的月份的時候,自然而然,就會寫新的庫了。
- 缺點,但是大部分的請求,都是訪問最新的數據。實際生產用 range,要看場景。
如果查詢條件不帶分片鍵,怎么辦?
當查詢不帶分片鍵時,則中間件一般會掃描所有庫表,然后聚合結果,然后進行返回。
對於大多數情況下,如果每個庫表的查詢速度還可以,返回結果的速度也是不錯的。具體,胖友可以根據自己的業務進行測試。
使用 user_id 分庫分表后,使用 id 查詢怎么辦?
有四種方案。
1)不處理
正如在 「如果查詢條件不帶分片鍵,怎么辦?」 問題所說,如果性能可以接受,可以不去處理。當然,前提是這樣的查詢不多,不會給系統帶來負擔。
2)映射關系
創建映射表,只有 id、user_id 兩列字段。使用 id 查詢時,先從映射表獲得 id 對應的 user_id ,然后再使用 id + user_id 去查詢對應的表。
當然,隨着業務量的增長,映射表也會越來越大,后續也可能需要進行分庫分表。
對於這方式,也可以有一些優化方案。
- 映射表改成緩存到 Redis 等 KV 緩存中。當然,需要考慮如果 Redis 持久化的情況。
- 將映射表緩存到內存中,減少一次到映射表的查詢。
3)基因 id
分庫基因:假如通過 user_id 分庫,分為 8 個庫,采用 user_id % 8 的方式進行路由,此時是由 user_id 的最后 3bit 來決定這行 User 數據具體落到哪個庫上,那么這 3bit 可以看為分庫基因。那么,如果我們將這 3 bit 參考類似 Snowflake 的方式,融入進入到 id 。
這里的 3 bit 只是舉例子,實際需要考慮自己分多少庫表,來決定到底使用多少 bit 。
上面的映射關系的方法需要額外存儲映射表,按非 user_id 字段查詢時,還需要多一次數據庫或 Cache 的訪問。通過基因 id ,就可以知道數據所在的庫表。
詳細說明,可以看看 《用 uid 分庫,uname 上的查詢怎么辦?》 文章。
目前,可以從 《大眾點評訂單系統分庫分表實踐》 文章中,看到大眾點評訂單使用了基因 id 。
4)多 sharding column
具體的內容,可以參考 《分庫分表的正確姿勢,你 GET 到了么?》 。當然,這種方案也是比較復雜的方案。
如何解決分布式事務?
目前市面上,分布式事務的解決方案還是蠻多的,但是都是基於一個前提,需要保證本地事務。那么,就對我們在分庫分表時,就有相應的要求:數據在分庫分表時,需要保證一個邏輯中,能夠形成本地事務。舉個例子,創建訂單時,我們會插入訂單表和訂單明細表,那么:
- 如果我們基於這兩個表的 id 進行分庫分表,將會導致插入的記錄被分到不同的庫表中,因為創建下單可以購買 n 個商品,那么就會有 1 條訂單記錄和 n 條 訂單明細記錄。而這 n 條訂單明細記錄無法和 1 條訂單記錄分到一個庫表中。
-
如果我們基於這兩個表的 user_id 進行分庫分表,那么插入的記錄被分到相同的庫表中。
這也是為什么業務表一般使用 user_id 進行分庫分表的原因之一。
可能會有胖友有疑問,為什么一定要形成本地事務?在有了本地事務的基礎上,通過使用分布式事務的解決方案,協調多個本地事務,形成最終一致性。另外, 本地事務在這個過程中,能夠保證萬一執行失敗,再重試時,不會產生臟數據。
彩蛋
參考與推薦如下文章:
- 《Sharding Sphere 官方文檔》
- boothsun 《分庫分表面試准備》
- butterfly100 《數據庫分庫分表思路》