設計模式_單例模式
一、背景
提起分庫分表,對於大部分伺服器開發來說,其實並不是一個新鮮的名詞。隨著業務的發展,我們表中的資料量會變的越來越大,欄位也可能隨著業務複雜度的升高而逐漸增多,我們為了解決單表的查詢效能問題,一般會進行分表操作。
同時我們業務的使用者活躍度也會越來越高,併發量級不斷加大,那麼可能會達到單個數據庫的處理能力上限。此時我們為了解決資料庫的處理效能瓶頸,一般會進行分庫操作。不管是分庫操作還是分表操作,我們一般都有兩種方式應對,一種是垂直拆分,一種是水平拆分。
關於兩種拆分方式的區別和特點,網際網路上參考資料眾多,很多人都寫過相關內容,這裡就不再進行詳細贅述,有興趣的讀者可以自行檢索。
此文主要詳細聊一聊,我們最實用最常見的水平分庫分表方式中的一些特殊細節,希望能幫助大家避免走彎路,找到最合適自身業務的分庫分表設計。
【注1】本文中的案例均基於Mysql資料庫,下文中的分庫分表統指水平分庫分表。
【注2】後文中提到到M庫N表,均指共M個數據庫,每個資料庫共N個分表,即總表個數其實為M*N。
二、什麼是一個好的分庫分表方案?
2.1 方案可持續性
前期業務資料量級不大,流量較低的時候,我們無需分庫分表,也不建議分庫分表。但是一旦我們要對業務進行分庫分表設計時,就一定要考慮到分庫分表方案的可持續性。
**那何為可持續性?**其實就是:業務資料量級和業務流量未來進一步升高達到新的量級的時候,我們的分庫分表方案可以持續使用。
一個通俗的案例,假定當前我們分庫分表的方案為10庫100表,那麼未來某個時間點,若10個庫仍然無法應對使用者的流量壓力,或者10個庫的磁碟使用即將達到物理上限時,我們的方案能夠進行平滑擴容。
在後文中我們將介紹下目前業界常用的翻倍擴容法和一致性Hash擴容法。
2.2 資料偏斜問題
一個良好的分庫分表方案,它的資料應該是需要比較均勻的分散在各個庫表中的。如果我們進行一個拍腦袋式的分庫分表設計,很容易會遇到以下類似問題:
a、某個資料庫例項中,部分表的資料很多,而其他表中的資料卻寥寥無幾,業務上的表現經常是延遲忽高忽低,飄忽不定。
b、資料庫叢集中,部分叢集的磁碟使用增長特別塊,而部分叢集的磁碟增長卻很緩慢。每個庫的增長步調不一致,這種情況會給後續的擴容帶來步調不一致,無法統一操作的問題。
這邊我們定義分庫分表最大資料偏斜率為 :(資料量最大樣本 - 資料量最小樣本)/ 資料量最小樣本。一般來說,如果我們的最大資料偏斜率在5%以內是可以接受的。
三、常見的分庫分表方案
3.1 Range分庫分表
顧名思義,該方案根據資料範圍劃分資料的存放位置。
舉個最簡單例子,我們可以把訂單表按照年份為單位,每年的資料存放在單獨的庫(或者表)中。如下圖所示:
/**
* 通過年份分表
*
* @param orderId
* @return
*/
public static String rangeShardByYear(String orderId) {
int year = Integer.parseInt(orderId.substring(0, 4));
return "t_order_" + year;
}
通過資料的範圍進行分庫分表,該方案是最樸實的一種分庫方案,它也可以和其他分庫分表方案靈活結合使用。時下非常流行的分散式資料庫:TiDB資料庫,針對TiKV中資料的打散,也是基於Range的方式進行,將不同範圍內的[StartKey,EndKey)分配到不同的Region上。
下面我們看看該方案的缺點:鄭州婦科醫院排名http://www.zzchxbyy120.com/
a、最明顯的就是資料熱點問題,例如上面案例中的訂單表,很明顯當前年度所在的庫表屬於熱點資料,需要承載大部分的IO和計算資源。
b、新庫和新表的追加問題。一般我們線上執行的應用程式是沒有資料庫的建庫建表許可權的,故我們需要提前將新的庫表提前建立,防止線上故障。
這點非常容易被遺忘,尤其是穩定跑了幾年沒有迭代任務,或者人員又交替頻繁的模組。
c、業務上的交叉範圍內資料的處理。舉個例子,訂單模組無法避免一些中間狀態的資料補償邏輯,即需要通過定時任務到訂單表中掃描那些長時間處於待支付確認等狀態的訂單。
這裡就需要注意了,因為是通過年份進行分庫分表,那麼元旦的那一天,你的定時任務很有可能會漏掉上一年的最後一天的資料掃描。
3.2 Hash分庫分表
雖然分庫分表的方案眾多,但是Hash分庫分表是最大眾最普遍的方案,也是本文花最大篇幅描述的部分。
針對Hash分庫分表的細節部分,相關的資料並不多。大部分都是闡述一下概念舉幾個示例,而細節部分並沒有特別多的深入,如果未結合自身業務貿然參考引用,後期非常容易出現各種問題。
在正式介紹這種分庫分表方式之前,我們先看幾個常見的錯誤案例。
常見錯誤案例一:非互質關係導致的資料偏斜問題
public static ShardCfg shard(String userId) {
int hash = userId.hashCode();
// 對庫數量取餘結果為庫序號
int dbIdx = Math.abs(hash % DB_CNT);
// 對錶數量取餘結果為表序號
int tblIdx = Math.abs(hash % TBL_CNT);
return new ShardCfg(dbIdx, tblIdx);
}
上述方案是初次使用者特別容易進入的誤區,用Hash值分別對分庫數和分表數取餘,得到庫序號和表序號。其實稍微思索一下,我們就會發現,以10庫100表為例,如果一個Hash值對100取餘為0,那麼它對10取餘也必然為0。
這就意味著只有0庫裡面的0表才可能有資料,而其他庫中的0表永遠為空!
類似的我們還能推導到,0庫裡面的共100張表,只有10張表中(個位數為0的表序號)才可能有資料。這就帶來了非常嚴重的資料偏斜問題,因為某些表中永遠不可能有資料,最大資料偏斜率達到了無窮大。
那麼很明顯,該方案是一個未達到預期效果的錯誤方案。資料的散落情況大致示意圖如下:
事實上,只要庫數量和表數量非互質關係,都會出現某些表中無資料的問題。
證明如下: