歡迎光臨
每天分享高質量文章

分庫分表實戰:可能是用戶表最佳分庫分表方案

再次丟擲筆者的觀點,在能滿足業務場景的情況下,單表>分割槽>單庫分表>分庫分表,推薦優先級從左到右逐漸降低。

本篇文章主要講用戶表(或者類似這種業務屬性的表)的分表方案,至於訂單表,流水錶等,本文的方案可能不是很合適,可以參考筆者另一篇文章《分庫分表技術演進&最佳實踐-修訂篇》。

我們首先來看一下分表時主要需要做的事情:

  1. 選定分片鍵:既然是用戶表那分片鍵非用戶ID莫屬;
  2. 修改代碼:以sharding-jdbc這種client樣式的中間件為例,主要是引入依賴,然後新增一些配置。業務代碼並不怎麼需要改動。
  3. 存量資料遷移;
  4. 業務發展超過容量評估後需要開發和運維介入擴容;

做過分庫分表的都知道,第3步最麻煩,而且非常不好驗證遷前後資料一致性(目前業界主流的遷移方案是存量資料遷移+利用binlog進行增量資料同步,待兩邊的資料持平後,將業務代碼中的開關切到分表樣式)。

第4步同樣麻煩,業務增長完全超過當初分表設計的容量評估是很常見的事情,這也成為業務高速發展的一個隱患。而且互聯網型別的業務都希望能做到7×24小時不停服務,這樣就給擴容帶來了更大的挑戰。筆者看過比較好的方案就是58沈劍提出的成倍擴容方案。如下圖所示,假設現在已經有2張表:tbuser1,tbuser2。且有兩個庫是主備關係,並且分表演算法是hash(user_id)%2:

現在要擴容到4張表,做法是將兩個庫的主從關係切斷。然後slave晉升為master,這樣就有兩個主庫:master-1,master-2。新的分表演算法是:

  • 庫選擇演算法為:hash(userid)%4的結果為1或者2,就選master-1庫,hash(userid)%4的結果為3或者0,就選master-2庫;
  • 表的選擇演算法為:hash(userid)%2的結果為1則選tbuser1表,hash(userid)%2的結果為0則選tbuser2表。

如此以來,兩個庫中總計4張表,都冗餘了1倍的資料:master-1中tbuser1冗餘了3、7、11…,master-1中tbuser2冗餘了4、8、12…,master-2中tbuser1冗餘了1、5、9…,master-2中tbuser2冗餘了2、6、10…。將這些冗餘資料刪掉後,庫、表、資料示意圖如下所示:

即使這樣方案,還是避免不了分表時的存量資料遷移,以及分表後業務發展到一定時期後的繁瑣擴容。那麼有沒有一種很好的方案,能夠一勞永逸,分表時不需要存量資料遷移,用戶量無論如何增長,擴容時都不需要遷移存量資料,只需要新增一個資料庫示例,修改一下配置即可。軟體開發行業,一個方案能撐過3~5年就是一個很優秀的方案,我們現在YY的是整個生命周期內都不用改動的完美的方案。沒錯,我們在尋找銀彈

這個方案筆者在兩個地方都接觸到了:

  1.  

    某V廠面試時,部門老大提出的方案;

     

  2.  

    和美團大牛普架討論瞭解到的CAT儲存方案;

     

說明:CAT是美團點評開源的APM,目前在Github上的star已經破萬(Github地址:https://github.com/dianping/cat),比skywalking和pinpoint還快,如果你正在選型APM,而且能接受代碼侵入,那麼CAT是一個不錯的選擇。

CAT儲存方案是按照寫入時間順序儲存,假設每小時寫入量是千萬級別,那麼分表就按照小時維度。也就是說,2019年7月18號10點資料寫入到表tbcatdata2019071810中,2019年7月18號12點資料寫入到表tbcatdata2019071812中,2019年7月20號14點資料寫入到表tbcatdata2019072014中。這樣做的優點如下:

  1. 歷史資料不用遷移;
  2. 擴容非常簡單;

缺點如下:

  1. 讀寫熱點集中,所有寫操作全部打在最新的表上。

有沒有發現,這個方案的優點就是我們需要的。BINGO,要的就是這樣的方案。那麼對應到用戶表上來具體的分表方案非常類似:按照range切分。需要說明的是,這個方案的前提是用戶ID一定要趨勢遞增,最好嚴格遞增。筆者給出3種用戶ID遞增的方案:

  • 自增ID

假設存量資料用戶表的id最大值是960W,那麼分表演算法是這樣的,表序號只需要根據user_id/10000000就能得到:

  1. 用戶ID在範圍[1, 10000000)中分到tbuser0中(需要將tbuser重命名為tbuser_0);
  2. 用戶ID在範圍[10000000, 20000000)中分到tbuser1中;
  3. 用戶ID在範圍[20000000, 30000000)中分到tbuser2中;
  4. 用戶ID在範圍[30000000, 40000000)中分到tbuser3中;
  5. 以此類推。

如果你的tbuser本來就有自增主鍵,那這種方案就比較好。但是需要註意幾點,由於用戶ID是自增的,所以這個ID不能通過HTTP暴露出去,否則可以通過新註冊一個用戶後,就能得到你的真實用戶數,這是比較危險的。其次,存量資料在單表中可以通過自增ID生成,但是當切換分表後,用戶ID如果還是用自增生成,需要註意在創建新表時設置AUTOINCREMENT,例如創建表tbuser2時,設置AUTO_INCREMENT=10000000,DDL如下:

  1. CREATE TABLE if not exists `tb_user_2` (
  2.   `id` int(11) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
  3.   `username` varchar(16) NOT NULL COMMENT '用戶名',
  4.   `remark` varchar(16) NOT NULL COMMENT '備註'
  5. ) ENGINE=InnoDB AUTO_INCREMENT=10000000;
  6.  
  7. - 這樣的話,當新增用戶時,用戶ID就會從10000000開始,而不會與之前的用戶ID衝突
  8. insert into tb_user_2 values(null, 'afei', 'afei');
  • Redis incr

第二種方案就是利用Redis的incr命令。將之前最大的ID儲存到Redis中,接下來新增用戶的ID值都通過incr命令得到。然後insert到表tbuser中。這種方案需要註意Redis主從切換後,晉升為主的Redis節點中的ID可能由於同步時間差不是最新ID的問題。這樣的話,可能會導致插入記錄到tbuser失敗。需要對這種異常特殊處理一下即可。

  • 利用雪花演算法生成

採用類雪花演算法生成用戶ID,這種方式不太好精確掌握切分表的時機。因為沒有高效獲取tbuser表資料量的辦法,也就不知道什麼時候表資料量達到1000w級別,也就不知道什麼時候需要往新表中插入資料(select count(*) from tbuser無論怎麼優化性能都不會很高,除非是MyISAM引擎)。而且如果利用雪花演算法生成用戶ID,那麼還需要一張表儲存用戶ID和分表關係:

筆者推薦第一種方案,即利用表自增ID生成用戶ID:方案越簡單,可靠性越高。其他兩種方案,或者其他方案或多或少需要引入一些中間件或者介質,從而增加方案的複雜度。新方案效果圖如下:

回顧總結

我們回頭看一下這種用戶表方案,滿足了存量資料不需要做任何遷移(除非是存量資料遠遠超過單表承受能力)。而且,無論用戶規模增長到多大量級,1億,10億,50億,後面都不需要做資料遷移。而且也不再需要開發和運維介入。因為整個方案,會自己往新表中插入資料。我們唯一需要做的就是,根據硬體性能,約定一個庫允許儲存的用戶表數量即可。假如一個庫儲存64張表,那麼當擴容到第65張表時,程式會自動往第二個庫的第一張表中寫入。

已同步到看一看
赞(0)

分享創造快樂