誰說一定要"分庫分表"?就用這篇懟回去!有理有據值得收藏。


摘自 堅持就是勝利 51CTO技術棧

當數據庫的數據量過大,大到一定的程度,我們就可以進行分庫分表。那麼基於什麼原則,什麼方法進行拆分,這就是本篇所要講的。

誰說一定要

不管是 IO 瓶頸還是 CPU 瓶頸,最終都會導致數據庫的活躍連接數增加,進而逼近甚至達到數據庫可承載的活躍連接數的閾值。


在業務 Service 來看, 就是可用數據庫連接少甚至無連接可用,接下來就可以想象了(併發量、吞吐量、崩潰)。


IO 瓶頸:

· 第一種:磁盤讀 IO 瓶頸,熱點數據太多,數據庫緩存放不下,每次查詢會產生大量的 IO,降低查詢速度→分庫和垂直分表。

· 第二種:網絡 IO 瓶頸,請求的數據太多,網絡帶寬不夠→分庫。


CPU 瓶頸:

· 第一種:SQL 問題:如 SQL 中包含 join,group by,order by,非索引字段條件查詢等,增加 CPU 運算的操作→SQL 優化,建立合適的索引,在業務 Service 層進行業務計算。

· 第二種:單表數據量太大,查詢時掃描的行太多,SQL 效率低,增加 CPU 運算的操作→水平分表。


分庫分表


水平分庫


誰說一定要

概念:以字段為依據,按照一定策略(hash、range等),將一個庫中的數據拆分到多個庫中。結果:

· 每個庫的結構都一樣

· 每個庫中的數據不一樣,沒有交集

· 所有庫的數據並集是全量數據


場景:系統絕對併發量上來了,分表難以根本上解決問題,並且還沒有明顯的業務歸屬來垂直分庫的情況下。


分析:庫多了,IO 和 CPU 的壓力自然可以成倍緩解。


水平分表


誰說一定要


概念:以字段為依據,按照一定策略(hash、range 等),將一個表中的數據拆分到多個表中。


結果:

· 每個表的結構都一樣。

· 每個表的數據不一樣,沒有交集,所有表的並集是全量數據。


場景:系統絕對併發量沒有上來,只是單表的數據量太多,影響了 SQL 效率,加重了 CPU 負擔,以至於成為瓶頸,可以考慮水平分表。


分析:單表的數據量少了,單次執行 SQL 執行效率高了,自然減輕了 CPU 的負擔。


垂直分庫


誰說一定要


概念:以表為依據,按照業務歸屬不同,將不同的表拆分到不同的庫中。結果:

· 每個庫的結構都不一樣。

· 每個庫的數據也不一樣,沒有交集。

· 所有庫的並集是全量數據。


場景:系統絕對併發量上來了,並且可以抽象出單獨的業務模塊的情況下。


分析:到這一步,基本上就可以服務化了。例如:隨著業務的發展,一些公用的配置表、字典表等越來越多,這時可以將這些表拆到單獨的庫中,甚至可以服務化。


再者,隨著業務的發展孵化出了一套業務模式,這時可以將相關的表拆到單獨的庫中,甚至可以服務化。


垂直分表


誰說一定要


概念:以字段為依據,按照字段的活躍性,將表中字段拆到不同的表中(主表和擴展表)。結果:

· 每個表的結構不一樣。

· 每個表的數據也不一樣,一般來說,每個表的字段至少有一列交集,一般是主鍵,用於關聯數據。

· 所有表的並集是全量數據。


場景:系統絕對併發量並沒有上來,表的記錄並不多,但是字段多,並且熱點數據和非熱點數據在一起,單行數據所需的存儲空間較大,以至於數據庫緩存的數據行減少,查詢時回去讀磁盤數據產生大量隨機讀 IO,產生 IO 瓶頸。


分析:可以用列表頁和詳情頁來幫助理解。垂直分表的拆分原則是將熱點數據(可能經常會查詢的數據)放在一起作為主表,非熱點數據放在一起作為擴展表,這樣更多的熱點數據就能被緩存下來,進而減少了隨機讀 IO。


拆了之後,要想獲取全部數據就需要關聯兩個表來取數據。但記住千萬別用 Join,因為 Join 不僅會增加 CPU 負擔並且會將兩個表耦合在一起(必須在一個數據庫實例上)。


關聯數據應該在 Service 層進行,分別獲取主表和擴展表的數據,然後用關聯字段關聯得到全部數據。


分庫分表工具


常用的分庫分表工具如下:

· Sharding-JDBC(噹噹)

· TSharding(蘑菇街)

· Atlas(奇虎 360)

· Cobar(阿里巴巴)

· MyCAT(基於 Cobar)

· Oceanus(58 同城)

· Vitess(谷歌) 各種工具的利弊自查


分庫分錶帶來的問題


分庫分表能有效緩解單機和單錶帶來的性能瓶頸和壓力,突破網絡 IO、硬件資源、連接數的瓶頸,同時也帶來一些問題,下面將描述這些問題和解決思路。


事務一致性問題


①分佈式事務

當更新內容同時存在於不同庫找那個,不可避免會帶來跨庫事務問題。跨分片事務也是分佈式事務,沒有簡單的方案,一般可使用"XA 協議"和"兩階段提交"處理。

分佈式事務能最大限度保證了數據庫操作的原子性。但在提交事務時需要協調多個節點,推後了提交事務的時間點,延長了事務的執行時間,導致事務在訪問共享資源時發生衝突或死鎖的概率增高。


隨著數據庫節點的增多,這種趨勢會越來越嚴重,從而成為系統在數據庫層面上水平擴展的枷鎖。


②最終一致性

對於那些性能要求很高,但對一致性要求不高的系統,往往不苛求系統的實時一致性,只要在允許的時間段內達到最終一致性即可,可採用事務補償的方式。


與事務在執行中發生錯誤立刻回滾的方式不同,事務補償是一種事後檢查補救的措施,一些常見的實現方法有:對數據進行對賬檢查,基於日誌進行對比,定期同標準數據來源進行同步等。

跨節點關聯查詢 Join 問題


切分之前,系統中很多列表和詳情表的數據可以通過 Join 來完成,但是切分之後,數據可能分佈在不同的節點上,此時 Join 帶來的問題就比較麻煩了,考慮到性能,儘量避免使用 Join 查詢。解決的一些方法:

①全局表

全局表,也可看做"數據字典表",就是系統中所有模塊都可能依賴的一些表,為了避免庫 Join 查詢,可以將這類表在每個數據庫中都保存一份。這些數據通常很少修改,所以不必擔心一致性的問題。

②字段冗餘


一種典型的反範式設計,利用空間換時間,為了性能而避免 Join 查詢。


例如,訂單表在保存 userId 的時候,也將 userName 也冗餘的保存一份,這樣查詢訂單詳情順表就可以查到用戶名 userName,就不用查詢買家 user 表了。


但這種方法適用場景也有限,比較適用依賴字段比較少的情況,而冗餘字段的一致性也較難保證。

③數據組裝

在系統 Service 業務層面,分兩次查詢,第一次查詢的結果集找出關聯的數據 id,然後根據 id 發起器二次請求得到關聯數據,最後將獲得的結果進行字段組裝。這是比較常用的方法。


④ER 分片

關係型數據庫中,如果已經確定了表之間的關聯關係(如訂單表和訂單詳情表),並且將那些存在關聯關係的表記錄存放在同一個分片上,那麼就能較好地避免跨分片 Join 的問題。


可以在一個分片內進行 Join,在 1:1 或 1:n 的情況下,通常按照主表的 ID 進行主鍵切分。

跨節點分頁、排序、函數問題


跨節點多庫進行查詢時,會出現 limit 分頁、order by 排序等問題。


分頁需要按照指定字段進行排序,當排序字段就是分頁字段時,通過分片規則就比較容易定位到指定的分片;當排序字段非分片字段時,就變得比較複雜。


需要先在不同的分片節點中將數據進行排序並返回,然後將不同分片返回的結果集進行彙總和再次排序。

最終返回給用戶如下圖:

誰說一定要


上圖只是取第一頁的數據,對性能影響還不是很大。但是如果取得頁數很大,情況就變得複雜的多。


因為各分片節點中的數據可能是隨機的,為了排序的準確性,需要將所有節點的前N頁數據都排序好做合併,最後再進行整體排序,這樣的操作很耗費 CPU 和內存資源,所以頁數越大,系統性能就會越差。在使用 Max、Min、Sum、Count 之類的函數進行計算的時候,也需要先在每個分片上執行相應的函數,然後將各個分片的結果集進行彙總再次計算。

全局主鍵避重問題


在分庫分表環境中,由於表中數據同時存在不同數據庫中,主鍵值平時使用的自增長將無用武之地,某個分區數據庫自生成 ID 無法保證全局唯一。因此需要單獨設計全局主鍵,避免跨庫主鍵重複問題。這裡有一些策略:


①UUID

UUID 標準形式是 32 個 16 進制數字,分為 5 段,形式是 8-4-4-4-12 的 36 個字符。


UUID 是最簡單的方案,本地生成,性能高,沒有網絡耗時,但是缺點明顯,佔用存儲空間多。


另外作為主鍵建立索引和基於索引進行查詢都存在性能問題,尤其是 InnoDb 引擎下,UUID 的無序性會導致索引位置頻繁變動,導致分頁。

②結合數據庫維護主鍵 ID 表


在數據庫中建立 sequence 表:


誰說一定要


stub 字段設置為唯一索引,同一 stub 值在 sequence 表中只有一條記錄,可以同時為多張表生辰全局 ID。


使用 MyISAM 引擎而不是 InnoDb,已獲得更高的性能。MyISAM 使用的是表鎖,對錶的讀寫是串行的,所以不用擔心併發時兩次讀取同一個 ID。


當需要全局唯一的 ID 時,執行:


誰說一定要


此方案較為簡單,但缺點較為明顯:存在單點問題,強依賴 DB,當 DB 異常時,整個系統不可用。配置主從可以增加可用性。另外性能瓶頸限制在單臺 MySQL 的讀寫性能。另有一種主鍵生成策略,類似 sequence 表方案,更好的解決了單點和性能瓶頸問題。這一方案的整體思想是:建立 2 個以上的全局 ID 生成的服務器,每個服務器上只部署一個數據庫,每個庫有一張 sequence 表用於記錄當前全局 ID。


表中增長的步長是庫的數量,起始值依次錯開,這樣就能將 ID 的生成散列到各個數據庫上。


誰說一定要

這種方案將生成 ID 的壓力均勻分佈在兩臺機器上,同時提供了系統容錯,第一臺出現了錯誤,可以自動切換到第二臺獲取 ID。


但有幾個缺點:系統添加機器,水平擴展較複雜;每次獲取 ID 都要讀取一次 DB,DB 的壓力還是很大,只能通過堆機器來提升性能。

③Snowflake 分佈式自增 ID 算法


誰說一定要

Twitter 的 Snowfalke 算法解決了分佈式系統生成全局 ID 的需求,生成 64 位 Long 型數字。


組成部分如下:

· 第一位未使用。

· 接下來的 41 位是毫秒級時間,41 位的長度可以表示 69 年的時間。

· 5 位 datacenterId,5 位 workerId。10 位長度最多支持部署 1024 個節點。

· 最後 12 位是毫秒內計數,12 位的計數順序號支持每個節點每毫秒產生 4096 個 ID 序列。


數據遷移、擴容問題


當業務高速發展、面臨性能和存儲瓶頸時,才會考慮分片設計,此時就不可避免的需要考慮歷史數據的遷移問題。一般做法是先讀出歷史數據,然後按照指定的分片規則再將數據寫入到各分片節點中。


此外還需要根據當前的數據量個 QPS,以及業務發展速度,進行容量規劃,推算出大概需要多少分片(一般建議單個分片的單表數據量不超過 1000W)。

什麼時候考慮分庫分表


①能不分就不分


並不是所有表都需要切分,主要還是看數據的增長速度。切分後在某種程度上提升了業務的複雜程度。不到萬不得已不要輕易使用分庫分表這個"大招",避免"過度設計"和"過早優化"。


分庫分表之前,先盡力做力所能及的優化:升級硬件、升級網絡、讀寫分離、索引優化等。當數據量達到單表瓶頸後,在考慮分庫分表。

②數據量過大,正常運維影響業務訪問

這裡的運維是指:

· 對數據庫備份,如果單表太大,備份時需要大量的磁盤 IO 和網絡 IO。

· 對一個很大的表做 DDL,MySQL會鎖住整個表,這個時間會很長,這段時間業務不能訪問此表,影響很大。

· 大表經常訪問和更新,就更有可能出現鎖等待。


③隨著業務發展,需要對某些字段垂直拆分

這裡就不舉例了,在實際業務中都可能會碰到,有些不經常訪問或者更新頻率低的字段應該從大表中分離出去。

④數據量快速增長

隨著業務的快速發展,單表中的數據量會持續增長,當性能接近瓶頸時,就需要考慮水平切分,做分庫分表了。


分享到:


相關文章: