分庫分表核心理念,你學會了嗎?
首先,我們需要知道所謂的"分庫分表",根本就不是一件事,而是三件事,它們要解決的問題也都不一樣。
這三件事分別是"只分庫不分表"、"只分表不分庫"、以及"既分庫又分表"。
什么時候分庫?
其實,分庫主要解決的是并發量大的問題。因為并發量一旦上來了,那么數據庫就可能會成為瓶頸,因為數據庫的連接數是有限的,雖然可以調整,但也不是無限調整的。
所以,當你的數據庫的讀或者寫的 QPS 過高,導致數據庫連接數不足的時候,就需要考慮分庫了,通過增加數據庫實例的方式來提供更多的可用數據庫連接,從而提升系統的并發度。
比較典型的分庫場景就是在做微服務拆分的時候,會按照業務邊界,把各個業務的數據從一個單一的數據庫中拆分開,分別把訂單、物流、商品、會員等單獨放到對應的數據庫中。
圖片
還有就是有的時候可能會把歷史訂單挪到歷史庫里面去。這也是分庫的一種具體做法。
什么時候分表?
分庫主要解決的是并發量大的問題,那分表其實主要解決的是數據量大的問題。
假如你的單表數據量非常大,因為并發不高,數據庫連接可能還夠,但是存儲和查詢的性能遇到了瓶頸,做了很多優化之后還是無法提升效率的時候,就需要考慮做分表了。
圖片
一般我們認為,單表行數超過 500 萬行或者單表容量超過 2GB 時,才需要考慮做分庫分表。
那我們是不是等到數據量到達 500 萬后,才開始分庫分表呢?
這個也不絕對,應該提前規劃分庫分表,如果估算 3 年后,表的數據量都不會到達 500 萬,則不需要分庫分表。
分庫分表的時候需要考慮數據未來 2~3 年的一個增量,即使現在數據量不多,但是每天的數據增量很可觀,幾個月之后就可以突破 500 萬上限,那么不是等到數據量到達 500 萬的時候才分庫分表,而是現在就應該考慮了。
什么時候既分庫又分表?
那么什么時候分庫又分表呢,那就是既需要解決并發量大的問題,又需要解決數據量大的問題的時候。通常情況下,高并發和大數據量的問題都是同時發生的,所以,我們會經常遇到分庫分表需要同時進行的情況。
橫向拆分 & 縱向拆分
談及到分庫分表,那就要涉及到該如何做拆分的問題。
通常在做拆分的時候有兩種分法,分別是橫向拆分(水平拆分)和縱向拆分(垂直拆分)。
假如我們有一張表,如果把這張表中某一條記錄的多個字段,拆分到多張表中,這種就是縱向拆分。那如果把一張表中的不同的記錄分別放到不同的表中,這種就是橫向拆分。
橫向拆分的結果是數據庫表中的數據會分散到多張分表中,使得每一個單表中的數據的條數都有所下降。比如我們可以把不同的用戶的訂單,分表拆分放到不同的表中。
圖片
縱向拆分的結果是數據庫表中的數據的字段數會變少,使得每一個單表中的數據的存儲有所下降。比如可以把商品詳情信息、價格信息、庫存信息等等分別拆分到不同的表中。
圖片
縱向拆分比較適合做冷熱分離,可以使得行數據變小,一個數據頁就能存放更多的數據,在查詢時就會減少I/O次數。
分表算法
選定了分表字段之后,如何基于這個分表字段來準確的把數據分表到某一張表中呢?
這就是分表算法要做的事情了,但是不管什么算法,我們都需要確保一個前提,那就是同一個分表字段,經過這個算法處理后,得到的結果一定是一致的,不可變的。
通常的分表算法有以下幾種:
Range 范圍
Range,即范圍策略劃分表。比如我們可以將表的主鍵 order_id,按照從 0~300萬 的劃分為一個表,300萬 ~ 600萬劃分到另外一個表。
有時候我們也可以按時間范圍來劃分,如不同年月的訂單放到不同的表。
- 優點:范圍分表,有利于擴容。
- 缺點:最近一段時間的數據都是匯聚在一張表里面,可能會有熱點問題。比如最近一個月的訂單都在 0~300萬之間,平時用戶一般都查最近一個月的訂單比較多,那么請求就都打到 order_01 了。
Hash 取模
Hash 取模策略:
指定的路由key(一般是 user_id、order_id 等作為key)對分表總數進行取模,把數據分散到各個表中。
比如原始訂單表信息,我們把它分成4張分表:
圖片
比如 id=1,對 4 取模,就會得到1,就把它放到 t_order_1 ;
一般,我們會取哈希值,再做取余:
Math.abs(orderId.hashCode()) % table_number
- 優點:Hash取模的方式,不會存在明顯的熱點問題。
- 缺點:如果未來某個時候,表數據量又到瓶頸了,需要擴容,就比較麻煩。所以一般建議提前規劃好,一次性分夠(可以考慮一致性哈希)。
一致性 Hash
為了解決 Hash 擴容的問題,我們可以采用一致性哈希的方式來做分表。
圖片
一致性哈希可以按照常用的 Hash 算法來將對應的 key 哈希到一個具有 2^32 次方個節點的空間中,形成一個順時針首尾相接的閉合環形,這個環稱為哈希環。
當添加一臺新的數據庫服務器時,只有增加服務器的位置和逆時針方向第一臺服務器之間的鍵會受影響。
簡單來說,一致性哈希算法能夠使機器節點的變動對整個集群的影響達到最小。
一致性哈希也存在一些問題,如:節點漂移、數據傾斜。這些都有對應的解決方案,這里不再贅述。
參考:一致性哈希問題及其解決方案。
斐波那契散列
前面幾種分表算法,大家會接觸多一點,斐波那契散列實際在分表算法中幾乎不被使用。
JDK 的 ThreadLocal 源碼中有一段有意思的代碼,如下所示:
圖片
定義了一個魔法值 HASH_INCREMENT = 0x61c88647,這個值被稱之為 “魔數”。
0x61c88647 與一個神奇的數字產生了聯系,它就是 (Math.sqrt(5) - 1)/2。也就是傳說中的黃金比例 0.618
(0.618 只是一個粗略值),即0x61c88647 = 2^32 * 黃金分割比
,同時也對應了上文提到的斐波那契散列。
它常用于在散列中增加哈希值。上面的代碼注釋中也解釋到是為了讓哈希碼能均勻的分布在 2 的 N 次方的數組里。
至于為什么使用斐波那契數列后散列更均勻,就涉及到相關數學問題了,此處不做更多解釋。
嚴格雪崩標準(SAC)
上面介紹了一些分表算法,那么一個好的分表算法有沒有參考標準呢?
在密碼學中,雪崩效應(avalanche effect)指加密算法的一種理想屬性。雪崩效應是指當輸入發生最微小的改變(例如,反轉一個二進制位)時,也會導致輸出的不可區分性改變(輸出中每個二進制位有50%的概率發生反轉)。
嚴格雪崩標準(SAC),建立于密碼學的完全性概念上,是雪崩效應的形式化。它指出,當任何一個輸入位被反轉時,輸出中的每一位均有 50% 的概率發生變化。
簡單來說,當我們對數據庫從 8庫32表 擴容到 16庫32表 的時候,每一個表中的數據總量都應該以 50% 的數量進行減少。這樣才是合理的。
引入嚴格雪崩標準(SAC) 之后,斐波那契散列是不滿足這個標準的,也就是說使用斐波那契散列,在分庫分表擴容情況下,可能導致數據分布不均勻,這也是為什么斐波那契散列幾乎不用于分表算法的原因。
訂單分庫分表實戰
背景:訂單表的讀寫場景復雜,?般有買家維度、賣家維度、訂單號維度 3 個主要維度。多讀寫維度情況下?論采取哪種維度做分庫分表,對另外兩種維度的查詢性能來說,基本都是災難。
解決方案:雙拆分列哈希(RANGE_HASH)。
選取兩個拆分鍵,兩個拆分鍵的后 N 位需確保一致,根據任一拆分鍵后 N 位計算哈希值,然后再按分庫數取模,完成路由計算。
先采用 RANGE_HASH 拆分算法按買家 id 后 N 位、訂單號后 N 位維度做分庫分表,作為買家表邏輯表。再用 HASH 拆分函數按商家 id 冗余一份數據,作為賣家表邏輯表。
訂單號生成規則需要根據買家表分表特性訂單號后 N 位等于買家 id 后 N 位做設計。
比如用戶id為 12345678,則用戶在下單時生成的單號為:xxxxxxxxx345678,單號前幾位可以根據公司自己規則設定,但是要注意不能重復。
全局 ID 的生成
涉及到分庫分表,就會引申出分布式系統中唯一主鍵 ID 的生成問題,有以下幾種方式:
UUID
UUID 是可以做到全局唯一的,而且生成方式也簡單,但是我們通常不推薦使用它做唯一ID,首先 UUID 太長了,其次字符串的查詢效率也比較慢,而且沒有業務含義,根本看不懂。
基于某個單表做自增主鍵
多張單表生成的自增主鍵會沖突,但是如果所有表的主鍵都從同一張表生成是不是就可以了。
所有的表在需要主鍵的時候,都到這張表中獲取一個自增的 ID。
這樣做是可以做到唯一,也能實現自增,但是問題是這個單表就變成整個系統的瓶頸,而且也存在單點問題,一旦他掛了,那整個數據庫就都無法寫入了。
雪花算法
圖片
雪花算法也是比較常用的一種分布式 ID 的生成方式,它具有全局唯一、遞增、高可用的特點。
雪花算法生成的主鍵主要由 4 部分組成,1bit 符號位、41bit 時間戳位、10bit 工作進程位以及 12bit 序列號位。
時間戳占用 41bit,精確到毫秒,總共可以容納約 69 年的時間。
工作進程位占用 10bit,其中高位 5bit 是數據中心 ID,低位 5bit 是工作節點 ID,最多可以容納 1024 個節點。
序列號占用 12bit,每個節點每毫秒從0開始不斷累加,最多可以累加到 4095,一共可以產生 4096 個 ID。
所以,雪花算法在同一毫秒內最多可以生成 1024 X 4096 = 4194304 個唯一的 ID。
時間回撥問題
熟悉雪花算法的可能了解到雪花算法存在名為“時間回撥” 的問題。
時間回撥:由于機器的時間是動態調整的,有可能會出現時間跑到之前幾毫秒,如果這個時候獲取到了這種時間,則會出現數據重復。
時間回撥問題解決思路可以參考美團開源的 Leaf。
美團 Leaf 引入了 Zookeeper 來解決時鐘回撥問題,其大致思路為:每個 Leaf 運行時定時向 zk 上報時間戳。每次 Leaf 服務啟動時,先校驗本機時間與上次發 ID 的時間,再校驗與 zk 上所有節點的平均時間戳。如果任何一個階段有異常,那么就啟動失敗報警。
這個解決方案還是比較好理解的,就是對比上次發 ID 的時間,還有其他機器的平均時間,通過本地存儲時間戳 + 定時上報時間戳的方式,解決了時間回撥的問題。
分庫分表遷移
有一個未分庫分表的系統,現在要分庫分表,如何才可以讓系統從未分庫分表切換到分庫分表上?
停機遷移方案
先說一個最 low 的方案,就是很簡單,大伙凌晨 12點 開始運維,網站或者 app 掛個公告,說 0 點到早上 6 點進行服務器維護,無法訪問......
接著到 0 點,停機,系統停掉,沒有流量寫入了,此時老的單庫單表數據庫靜止了。然后提前寫好一個導數的一次性工具,此時直接跑起來,然后將單庫單表的數據讀出來,寫到分庫分表里面去。
導數完了之后,就 ok 了,修改系統的數據庫連接配置啥的,包括可能代碼和 SQL 也許有修改,那你就用最新的代碼,然后直接啟動連到新的分庫分表上去。
但是這個方案比較 low,有個致命的問題就是業務要中斷,來看看高大上一點的方案。
雙寫遷移方案
這個是常用的一種遷移方案,比較靠譜一些,不用停機。
大致步驟如下:
- 先改造我們的數據寫入端, 使數據同時寫入舊數據庫和新數據庫。
- 對存量數據進行不停機的遷移。
- 等到雙寫服務運行一段時間,再次進行舊數據和新數據的校驗同步。
- 完全切換讀取的數據源為新數據庫,關閉舊數據庫的寫入和讀取,下線舊數據庫。
這種方式的好處是:遷移的過程可以隨時回滾,將遷移的風險降到了最低。劣勢是:時間周期比較長,應用有改造的成本。
分庫分表帶來的問題
分庫分表之后,會帶來很多問題。
首先,做了分庫分表之后,所有的讀和寫操作,都需要帶著分表字段,這樣才能知道具體去哪個庫、哪張表中去查詢數據。如果不帶的話,就得支持全表掃描。
還有,一旦我們要從多個數據庫中查詢或者寫入數據,就有很多事情都不能做了,比如跨庫事務就是不支持的。
圖片
所以,分庫分表之后就會帶來因為不支持事務而導致的數據一致性的問題。
其次,做了分庫分表之后,以前單表中很方便的分頁查詢、排序等等操作就都失效了。因為我們不能跨多表進行分頁、排序。
總之,分庫分表雖然能解決一些大數據量、高并發的問題,但是同時也會帶來一些新的問題。所以,在做數據庫優化的時候,還是建議大家優先選擇其他的優化方式,最后再考慮分庫分表。