1. 程式人生 > >一次難得的分庫分表實踐

一次難得的分庫分表實踐

背景

前不久發過兩篇關於分表的文章:

  • 一次分表踩坑實踐的探討
  • 分表後需要注意的二三事

從標題可以看得出來,當時我們只做了分表;還是由於業務發展,截止到現在也做了分庫,目前看來都還比較順利,所以藉著腦子還記得清楚來一次覆盤。

先來回顧下整個分庫分表的流程如下:

整個過程也很好理解,基本符合大部分公司的一個發展方向。

很少會有業務一開始就會設計為分庫分表,雖說這樣會減少後續的坑,但部分公司剛開始都是以業務為主。

直到業務發展到單表無法支撐時,自然而然會考慮分表甚至分庫的事情。

於是本篇會作一次總結,之前提過的內容可能會再重複一次。

分表

首先討論下什麼樣的情況下適合分表?

根據我的經驗來看,當某張表的資料量已經達到千萬甚至上億,同時日增資料量在 2% 以上。

當然這些數字並不是絕對的,最重要的還是對這張表的寫入和查詢都已經影響到正常業務執行,比如查詢速度明顯下降,資料庫整體 IO 居高不下等。

而談到分表時我們著重討論的還是水平分表;

也就是將一張大表資料通過某種路由演算法將資料儘可能的均勻分配到 N 張小表中。

Range

而分表策略也有好幾種,分別適用不同的場景。

首先第一種是按照範圍劃分,比如我們可以將某張表的建立時間按照日期劃分存為月表;也可以將某張表的主鍵按照範圍劃分,比如 【1~10000】在一張表,【10001~20000】在一張表,以此類推。

這樣的分表適合需要對資料做歸檔處理,比如系統預設只提供近三個月曆史資料的查詢功能,這樣也方便操作;只需要把三月之前的資料單獨移走備份儲存即可)。

這個方案有好處也有弊端:

  • 好處是自帶水平擴充套件,不需要過多幹預。
  • 缺點是可能會出現資料不均勻的情況(比如某個月請求暴增)。

Hash

按照日期這樣的範圍分表固然簡單,但適用範圍還是比較窄;畢竟我們大部分的資料查詢都不想帶上時間。

比如某個使用者想查詢他產生的所有訂單資訊,這是很常見的需求。

於是我們分表的維度就得改改,分表演算法可以採用主流的 hash+mod 的組合。

這是一個經典的演算法,大名鼎鼎的 HashMap 也是這樣來儲存資料。

假設我們這裡將原有的一張大表訂單資訊分為 64 張分表:

這裡的 hash 便是將我們需要分表的欄位進行一次雜湊運算,使得經過雜湊的資料儘可能的均勻並且不重複。

當然如果本身這個欄位就是一個整形並且不重複也可以省略這個步驟,直接進行 Mod 得到分表下標即可。

分表數量選擇

至於這裡的分表數量(64)也是有講究的,具體設為多少這個沒有標準值,需要根據自身業務發展,資料增量進行預估。

根據我個人的經驗來看,至少需要保證分好之後的小表在業務發展的幾年之內都不會出現單表資料量過大(比如達到千萬級)。

我更傾向於在資料庫可接受的範圍內儘可能的增大這個分表數,畢竟如果後續小表也達到瓶頸需要再進行一次分表擴容,那是非常痛苦的。

目前筆者還沒經歷這一步,所以本文沒有相關介紹。

但是這個數量又不是瞎選的,和 HashMap 一樣,也建議得是 2^n,這樣可以方便在擴容的時儘可能的少遷移資料。

Range + Hash

當然還有一種思路,RangeHash 是否可以混用。

比如我們一開始採用的是 Hash 分表,但是資料增長巨大,導致每張分表資料很快達到瓶頸,這樣就不得不再做擴容,比如由 64 張表擴容到 256 張。

但擴容時想要做到不停機遷移資料非常困難,即便是停機,那停多久呢?也不好說。

所以我們是否可以在 Mod 分表的基礎上再分為月表,藉助於 Range 自身的擴充套件性就不用考慮後續資料遷移的事情了。

這種方式理論可行,但我沒有實際用過,給大家的思路做個參考吧。

煩人的資料遷移

分表規則弄好後其實只是完成了分表的第一步,真正麻煩的是資料遷移,或者說是如何做到對業務影響最小的資料遷移。

除非是一開始就做了分表,所以資料遷移這一步驟肯定是跑不掉的。

下面整理下目前我們的做法供大家參考:

  1. 一旦分表上線後所有的資料寫入、查詢都是針對於分表的,所以原有大表內的資料必須得遷移到分表裡,不然對業務的影響極大。
  2. 我們估算了對一張 2 億左右的表進行遷移,自己寫的遷移程式,大概需要花 4~5 天的時間才能完成遷移。
  3. 意味著這段時間內,以前的資料對使用者是不可見的,顯然這樣業務不能接受。
  4. 於是我們做了一個相容處理:分表改造上線後,所有新產生的資料寫入分表,但對歷史資料的操作還走老表,這樣就少了資料遷移這一步驟。
  5. 只是需要在操作資料之前做一次路由判斷,當新資料產生的足夠多時(我們是兩個月時間),幾乎所有的操作都是針對於分表,再從庫啟動資料遷移,資料遷移完畢後將原有的路由判斷去掉。
  6. 最後所有的資料都從分表產生和寫入。

至此整個分表操作完成。

業務相容

同時分表之後還需要相容其他業務;比如原有的報表業務、分頁查詢等,現在來看看我們是如何處理的。

報表

首先是報表,沒分表之前之間查詢一張表就搞定了,現在不同,由一張表變為 N 張表。

所以原有的查詢要改為遍歷所有的分表,考慮到效能可以利用多執行緒併發查詢分表資料然後彙總。

不過只依靠 Java 來對這麼大量的資料做統計分析還是不現實,剛開始可以應付過去,後續還得用上大資料平臺來處理。

查詢

再一個是查詢,原有的分頁查詢肯定是不能用了,畢竟對上億的資料分頁其實沒什麼意義。

只能提供通過分表字段的查詢,比如是按照訂單 ID 分表,那查詢條件就得帶上這個欄位,不然就會涉及到遍歷所有表。

這也是所有分表之後都會遇到的一個問題,除非不用 MySQL 這類關係型資料庫。

分庫

分表完成後可以解決單表的壓力,但資料庫本身的壓力卻沒有下降。

我們在完成分表之後的一個月內又由於資料庫裡“其他表”的寫入導致整個資料庫 IO 增加,而且這些“其他表”還和業務關係不大。

也就是說一些可有可無的資料導致了整體業務受影響,這是非常不划算的事情。

於是我們便把這幾張表單獨移到一個新的資料庫中,完全和現有的業務隔離開來。

這樣就會涉及到幾個改造:

  1. 應用自身對這些資料的查詢、寫入都要改為呼叫一個獨立的 Dubbo 服務,由這個服務對遷移的表進行操作。
  2. 暫時不做資料遷移,所以查詢時也得按照分表那樣做一個相容,如果查詢老資料就要在當前庫查詢,新資料就要呼叫 Dubbo 介面進行查詢。
  3. 對這些表的一些關聯查詢也得改造為查詢 Dubbo 介面,在記憶體中進行拼接即可。
  4. 如果資料量確實很大,也可將同步的 Dubbo 介面換為寫入訊息佇列來提高吞吐量。

目前我們將這類資料量巨大但對業務不太影響的表單獨遷到一個庫後,資料庫的整體 IO 下降明顯,業務也恢復正常。

總結

最後我們還需要做一步歷史資料歸檔的操作,將 N 個月之前的資料要定期遷移到 HBASE 之類儲存,保證 MySQL 中的資料一直保持在一個可接受的範圍。

而歸檔資料的查詢便依賴於大資料提供服務。

本次分庫分表是一次非常難得的實踐操作,網上大部分的資料都是在汽車出廠前就換好了輪胎。

而我們大部分碰到的場景都是要對高速路上跑著的車子換胎,一不小心就“車毀人亡”。

有更好的方式方法歡迎大家評論區留言討論。

你的點贊與分享是對我最大的支援