分庫分表
讀寫分離主要應對的是資料庫讀併發,沒有解決資料庫儲存問題。試想一下:如果 MySQL 一張表的資料量過大怎麼辦?
換言之,我們該如何解決 MySQL 的儲存壓力呢?
答案之一就是 分庫分表。
什麼是分庫?
分庫 就是將資料庫中的資料分散到不同的資料庫上,可以垂直分庫,也可以水平分庫。
垂直分庫 就是把單一資料庫按照業務進行劃分,不同的業務使用不同的資料庫,進而將一個資料庫的壓力分擔到多個資料庫。
舉個例子:說你將資料庫中的使用者表、訂單表和商品表分別單獨拆分為使用者資料庫、訂單資料庫和商品資料庫。
垂直分庫
垂直分庫
水平分庫 是把同一個表按一定規則拆分到不同的資料庫中,每個庫可以位於不同的伺服器上,這樣就實現了水平擴充套件,解決了單表的儲存和效能瓶頸的問題。
舉個例子:訂單表資料量太大,你對訂單表進行了水平切分(水平分表),然後將切分後的 2 張訂單表分別放在兩個不同的資料庫。
什麼是分表?
分表 就是對單表的資料進行拆分,可以是垂直拆分,也可以是水平拆分。
垂直分表 是對資料表列的拆分,把一張列比較多的表拆分為多張表。
舉個例子:我們可以將使用者資訊表中的一些列單獨抽出來作為一個表。
水平分表 是對資料錶行的拆分,把一張行比較多的表拆分為多張表,可以解決單一表資料量過大的問題。
舉個例子:我們可以將使用者資訊表拆分成多個使用者資訊表,這樣就可以避免單一表資料量過大對效能造成影響。
水平拆分只能解決單表資料量大的問題,為了提升效能,我們通常會選擇將拆分後的多張表放在不同的資料庫中。也就是說,水平分表通常和水平分庫同時出現。
什麼情況下需要分庫分表?
遇到下面幾種場景可以考慮分庫分表:
單表的資料達到千萬級別以上,資料庫讀寫速度比較緩慢。
資料庫中的資料佔用的空間越來越大,備份時間越來越長。
應用的併發量太大(應該優先考慮其他效能最佳化方法,而非分庫分表)。
不過,分庫分表的成本太高,如非必要儘量不要採用。而且,並不一定是單表千萬級資料量就要分表,畢竟每張表包含的欄位不同,它們在不錯的效能下能夠存放的資料量也不同,還是要具體情況具體分析。
之前看過一篇文章分析 “InnoDB 中高度為 3 的 B+ 樹最多可以存多少資料”,寫的挺不錯,感興趣的可以看看。
常見的分片演算法有哪些?
分片演算法主要解決了資料被水平分片之後,資料究竟該存放在哪個表的問題。
常見的分片演算法有:
雜湊分片:求指定分片鍵的雜湊,然後根據雜湊值確定資料應被放置在哪個表中。雜湊分片比較適合隨機讀寫的場景,不太適合經常需要範圍查詢的場景。雜湊分片可以使每個表的資料分佈相對均勻,但對動態伸縮(例如新增一個表或者庫)不友好。
範圍分片:按照特定的範圍區間(比如時間區間、ID 區間)來分配資料,比如 將 id 為 1~299999 的記錄分到第一個表, 300000~599999 的分到第二個表。範圍分片適合需要經常進行範圍查詢且資料分佈均勻的場景,不太適合隨機讀寫的場景(資料未被分散,容易出現熱點資料的問題)。
對映表分片:使用一個單獨的表(稱為對映表)來儲存分片鍵和分片位置的對應關係。對映表分片策略可以支援任何型別的分片演算法,如雜湊分片、範圍分片等。對映表分片策略是可以靈活地調整分片規則,不需要修改應用程式程式碼或重新分佈資料。不過,這種方式需要維護額外的表,還增加了查詢的開銷和複雜度。
一致性雜湊分片:將雜湊空間組織成一個環形結構,將分片鍵和節點(資料庫或表)都對映到這個環上,然後根據順時針的規則確定資料或請求應該分配到哪個節點上,解決了傳統雜湊對動態伸縮不友好的問題。
地理位置分片:很多 NewSQL 資料庫都支援地理位置分片演算法,也就是根據地理位置(如城市、地域)來分配資料。
融合演算法分片:靈活組合多種分片演算法,比如將雜湊分片和範圍分片組合。
……
分片鍵如何選擇?
分片鍵(Sharding Key)是資料分片的關鍵欄位。分片鍵的選擇非常重要,它關係著資料的分佈和查詢效率。一般來說,分片鍵應該具備以下特點:
具有共性,即能夠覆蓋絕大多數的查詢場景,儘量減少單次查詢所涉及的分片數量,降低資料庫壓力;
具有離散性,即能夠將資料均勻地分散到各個分片上,避免資料傾斜和熱點問題;
具有穩定性,即分片鍵的值不會發生變化,避免資料遷移和一致性問題;
具有擴充套件性,即能夠支援分片的動態增加和減少,避免資料重新分片的開銷。
實際專案中,分片鍵很難滿足上面提到的所有特點,需要權衡一下。並且,分片鍵可以是表中多個欄位的組合,例如取使用者 ID 後四位作為訂單 ID 字尾。
分庫分表會帶來什麼問題呢?
記住,你在公司做的任何技術決策,不光是要考慮這個技術能不能滿足我們的要求,是否適合當前業務場景,還要重點考慮其帶來的成本。
引入分庫分表之後,會給系統帶來什麼挑戰呢?
join 操作:同一個資料庫中的表分佈在了不同的資料庫中,導致無法使用 join 操作。這樣就導致我們需要手動進行資料的封裝,比如你在一個資料庫中查詢到一個資料之後,再根據這個資料去另外一個資料庫中找對應的資料。不過,很多大廠的資深 DBA 都是建議儘量不要使用 join 操作。因為 join 的效率低,並且會對分庫分表造成影響。對於需要用到 join 操作的地方,可以採用多次查詢業務層進行資料組裝的方法。不過,這種方法需要考慮業務上多次查詢的事務性的容忍度。
事務問題:同一個資料庫中的表分佈在了不同的資料庫中,如果單個操作涉及到多個資料庫,那麼資料庫自帶的事務就無法滿足我們的要求了。這個時候,我們就需要引入分散式事務了。關於分散式事務常見解決方案總結,網站上也有對應的總結:https://javaguide.cn/distributed-system/distributed-transaction.html 。
分散式 ID:分庫之後, 資料遍佈在不同伺服器上的資料庫,資料庫的自增主鍵已經沒辦法滿足生成的主鍵唯一了。我們如何為不同的資料節點生成全域性唯一主鍵呢?這個時候,我們就需要為我們的系統引入分散式 ID 了。關於分散式 ID 的詳細介紹&實現方案總結,可以看我寫的這篇文章:分散式 ID 介紹&實現方案總結。
跨庫聚合查詢問題:分庫分表會導致常規聚合查詢操作,如 group by,order by 等變得異常複雜。這是因為這些操作需要在多個分片上進行資料彙總和排序,而不是在單個資料庫上進行。為了實現這些操作,需要編寫複雜的業務程式碼,或者使用中介軟體來協調分片間的通訊和資料傳輸。這樣會增加開發和維護的成本,以及影響查詢的效能和可擴充套件性。
……
另外,引入分庫分表之後,一般需要 DBA 的參與,同時還需要更多的資料庫伺服器,這些都屬於成本。
分庫分表有沒有什麼比較推薦的方案?
Apache ShardingSphere 是一款分散式的資料庫生態系統, 可以將任意資料庫轉換為分散式資料庫,並透過資料分片、彈性伸縮、加密等能力對原有資料庫進行增強。
ShardingSphere 專案(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar)是噹噹捐入 Apache 的,目前主要由京東數科的一些巨佬維護。
ShardingSphere 絕對可以說是當前分庫分表的首選!ShardingSphere 的功能完善,除了支援讀寫分離和分庫分表,還提供分散式事務、資料庫治理、影子庫、資料加密和脫敏等功能。
ShardingSphere 提供的功能如下:
ShardingSphere 提供的功能
ShardingSphere 提供的功能
ShardingSphere 的優勢如下(摘自 ShardingSphere 官方文件:https://shardingsphere.apache.org/document/current/cn/overview/):
極致效能:驅動程式端歷經長年打磨,效率接近原生 JDBC,效能極致。
生態相容:代理端支援任何透過 MySQL/PostgreSQL 協議的應用訪問,驅動程式端可對接任意實現 JDBC 規範的資料庫。
業務零侵入:面對資料庫替換場景,ShardingSphere 可滿足業務無需改造,實現平滑業務遷移。
運維低成本:在保留原技術棧不變前提下,對 DBA 學習、管理成本低,互動友好。
安全穩定:基於成熟資料庫底座之上提供增量能力,兼顧安全性及穩定性。
彈性擴充套件:具備計算、儲存平滑線上擴充套件能力,可滿足業務多變的需求。
開放生態:透過多層次(核心、功能、生態)外掛化能力,為使用者提供可定製滿足自身特殊需求的獨有系統。
另外,ShardingSphere 的生態體系完善,社群活躍,文件完善,更新和釋出比較頻繁。
不過,還是要多提一句:現在很多公司都是用的類似於 TiDB 這種分散式關係型資料庫,不需要我們手動進行分庫分表(資料庫層面已經幫我們做了),也不需要解決手動分庫分表引入的各種問題,直接一步到位,內建很多實用的功能(如無感擴容和縮容、冷熱儲存分離)!如果公司條件允許的話,個人也是比較推薦這種方式!
分庫分表後,資料怎麼遷移呢?
分庫分表之後,我們如何將老庫(單庫單表)的資料遷移到新庫(分庫分表後的資料庫系統)呢?
比較簡單同時也是非常常用的方案就是停機遷移,寫個指令碼老庫的資料寫到新庫中。比如你在凌晨 2 點,系統使用的人數非常少的時候,掛一個公告說系統要維護升級預計 1 小時。然後,你寫一個指令碼將老庫的資料都同步到新庫中。
如果你不想停機遷移資料的話,也可以考慮雙寫方案。雙寫方案是針對那種不能停機遷移的場景,實現起來要稍微麻煩一些。具體原理是這樣的:
我們對老庫的更新操作(增刪改),同時也要寫入新庫(雙寫)。如果操作的資料不存在於新庫的話,需要插入到新庫中。 這樣就能保證,咱們新庫裡的資料是最新的。
在遷移過程,雙寫只會讓被更新操作過的老庫中的資料同步到新庫,我們還需要自己寫指令碼將老庫中的資料和新庫的資料做比對。如果新庫中沒有,那咱們就把資料插入到新庫。如果新庫有,舊庫沒有,就把新庫對應的資料刪除(冗餘資料清理)。
重複上一步的操作,直到老庫和新庫的資料一致為止。
想要在專案中實施雙寫還是比較麻煩的,很容易會出現問題。我們可以藉助上面提到的資料庫同步工具 Canal 做增量資料遷移(還是依賴 binlog,開發和維護成本較低)。
總結
讀寫分離主要是為了將對資料庫的讀寫操作分散到不同的資料庫節點上。 這樣的話,就能夠小幅提升寫效能,大幅提升讀效能。
讀寫分離基於主從複製,MySQL 主從複製是依賴於 binlog 。
分庫 就是將資料庫中的資料分散到不同的資料庫上。分表 就是對單表的資料進行拆分,可以是垂直拆分,也可以是水平拆分。
引入分庫分表之後,需要系統解決事務、分散式 id、無法 join 操作問題。
現在很多公司都是用的類似於 TiDB 這種分散式關係型資料庫,不需要我們手動進行分庫分表(資料庫層面已經幫我們做了),也不需要解決手動分庫分表引入的各種問題,直接一步到位,內建很多實用的功能(如無感擴容和縮容、冷熱儲存分離)!如果公司條件允許的話,個人也是比較推薦這種方式!
如果必須要手動分庫分表的話,ShardingSphere 是首選!ShardingSphere 的功能完善,除了支援讀寫分離和分庫分表,還提供分散式事務、資料庫治理等功能。另外,ShardingSphere 的生態體系完善,社群活躍,文件完善,更新和釋出比較頻繁。