要解決資料表被水平拆分後的單表查詢問題,我們首先要回到問題的源頭,我們為什麼需要將資料庫的表進行水平拆分。下麵我們來推導下我們最終下定決心做水平拆分表的演進過程,具體如下:
第一個演進過程:進行了讀寫分離的表在資料增長後需要進行水平拆分嗎?回答這個疑問我們首先要想想進行讀寫分離操作的表真的是因為資料量大嗎?答案其實是否定的。最基本的讀寫分離的目的是為瞭解決資料庫的某張表讀寫比率嚴重失衡的問題,舉個例子,有一張表每天會增加1萬條資料,也就是說我們的系統每天會向這張表做1萬次寫的操作,當然也有可能我們還會更新或者刪除這張表的某些已有的記錄,這些操作我們把它歸併到寫操作,那麼這張表一天我們隨意定義個估值吧2萬5千次寫操作,其實這種表的資料量並不大,一年下來也就新增的幾百萬條資料,一個大型的商業級別的關係資料庫,當我們為表建立好索引和分割槽後,查詢幾百萬條資料它的效率並不低,這麼說來查詢的效率問題還不一定是讀寫分離的源頭。其實啊,這張表除了寫操作每天還承受的讀操作可能會是10萬,20萬甚至更高,這個時候問題來了,像oracle和mysql這樣鼎鼎大名的關係資料庫預設的最大連線數是100,一般上了生產環境我們可能會設定為150或者200,這些連線數已經到了這些關係資料庫的最大極限了,如果再加以提升,資料庫效能會嚴重下降,最終很有可能導致資料庫由於壓力過大而變成了一個巨鎖,最終導致系統發生503的錯誤,如是我們就會想到採用讀寫分離方案,將資料庫的讀操作遷移到專門的讀庫裡,如果系統的負載指標和我列舉的例子相仿,那麼遷移的讀庫甚至不用做什麼垂直拆分就能滿足實際的業務需求,因為我們的目的只是為了減輕資料庫的連線壓力。
第二個演進過程:隨著公司業務的不斷增長,系統的執行的壓力也越來越大了,我們已經瞭解了系統的第一個瓶頸是從儲存開始了,如是我們開始談論方案如何解決儲存的問題,這時我們發現我們已經做了讀寫分離,也使用了快取,甚至連搜尋技術也用上了,那麼下個階段就是垂直拆分了,垂直拆分很簡單就是把表從資料庫裡拆出來,單獨建庫建表,但是這種直截了當的方案想想就能感到這樣的做法似乎沒有打中系統的痛點,那麼系統的痛點到底是什麼呢?根據資料庫本身的特性,我們會發現痛點主要是三個方面組成:
第一個方面:資料庫的連線數的限制。原庫的某些表可能承擔資料庫80%的連線,極端下甚至可以超過90%的連線,而且這些表的業務操作十分的頻繁,當其他小眾業務的表需要進行操作時候,搞不好因為連線數被全部佔用而不得不排隊等待空閑連線的出現,那麼這個時候我們就會考慮把這張表做垂直拆分,這樣就減輕了原資料庫連線的壓力,使得資料庫連線負載變得比較均衡。
第二個方面是資料庫的讀操作,第三個方面是資料庫的寫操作,雖然把讀和寫分成兩個方面,但是這兩個方面在我們做垂直拆分時候要結合起來考慮。首先我們要分析下資料庫的寫操作,單獨的寫操作效率都是很高的,不管我們的寫是單條記錄的寫操作,還是批次的寫操作,這些寫操作的資料量就是我們要去寫的資料的大小,因此控制寫的資料量的大小是一件很容易很天然的操作,所以這些操作不會造成資料庫太大負擔,詳細點的話,對於資料庫而言,新增操作無非是在原來資料後面追加些記錄,而修改操作或者刪除操作一般都是透過建立了高效索引的欄位來定位資料後再進行的操作,因此它的效能也是非常高的。而讀操作看起來比寫操作簡單(例如:讀操作不存在像事務這些烏七八糟因素的幹擾),但是當讀操作面對海量資料時候就嚴重挑戰著資料庫和硬碟的極限能力,因此讀操作很容易產生瓶頸問題,而且這個瓶頸不管問題表是否讀寫失衡都會面臨的。
前文裡我詳細列舉了一個交易表設計的案例,其中我們可以看到資料庫垂直拆分在實際應用裡的運用,在例子裡我們首先根據業務特點將交易表分成了實時交易表和歷史交易表,這個做法其實就是將原交易表的讀和寫進行分離,但是這種分離和純粹的讀寫分離相比會更加有深意,這個深意就是拆分實時和歷史交易表也就是在分拆原表的讀寫操作的關聯性,換句話說,如果我們不這麼做的話,那麼交易表的每次寫和每次讀幾乎等價,這樣我們沒法單獨解決讀的效能問題,分出了歷史交易表後我們再對歷史交易表來做讀的最佳化,那麼這也不會影響到寫操作,這樣把問題的複雜度給降低了。在案例裡我們對歷史交易表進行了業務級別的水平拆分,但是這個拆分是以如何提升讀的效率進行的,因此前文講到的水平拆分裡主鍵設計方案基本上派不上用場,因為這兩種水平拆分的出發點是不同的,那麼使用的手段和達到效果也將不一樣。
由上所述,我們可以把資料庫的水平拆分重新定義下,我在這幾篇文章裡一直講述的水平拆分本質是從資料庫技術來定義的,我把它們稱為狹義的水平拆分,與狹義相對的就是廣義的水平拆分,例如上文例子裡把交易表根據業務特性分為實時交易表和歷史交易表,這種行為也是一種水平拆分,但是這個拆分不會遵守我前面講到主鍵設計方案,但是它的確達到水平拆分的目的,所以這樣的水平拆分就屬於廣義的水平拆分了。
第三個演進過程:到了三個演進過程我們就會考慮到真正的水平拆分了,也就是上面提到的狹義的水平拆分了,狹義的水平拆分執行的理由有兩個,一個那就是資料量太大了,另一個是資料表的讀寫的關聯性很難進行拆分了,這點和垂直拆分有所不同,做垂直拆分的考慮不一定是因為資料量過大,例如某種表資料量不大,但是負載過重,很容易讓資料庫達到連線的極限值,我們也會採取垂直拆分手段來解決問題,此外,我們想減輕寫操作和讀操作的關聯性,從而能單獨對有瓶頸的寫操作或讀操作做最佳化設計,那麼我們也會考慮到垂直拆分,當然資料量實在是太大的表我們想最佳化,首先也會考慮到垂直拆分,因為垂直拆分是針對海量資料最佳化的起始手段,但是垂直拆分可不一定能解決海量資料的問題。
狹義水平拆分的使用的前提是因為資料量太大,到底多大了,我們舉個例子來說明下,假如某個電商平臺一天的交易筆數有2億筆,我們用來儲存資料的關係資料庫單表記錄到了5千萬條後,查詢效能就會嚴重下降,那麼如果我們把這兩億條資料全部存進這個資料庫,那麼隨著資料的累積,實時交易查詢基本已經沒法正常完成了,這個時候我們就得考慮把實時交易表進行狹義的水平拆分,狹義的水平拆分首先碰到的難點就是主鍵設計的問題,主鍵設計問題也就說明狹義水平拆分其實解決的是海量資料寫的問題,如果這張表讀操作很少,或者基本沒有,這個水平拆分是很好設計的,但是一張表只寫不讀,對於作為業務系統的後臺資料庫那基本是非常罕見的,。
前文講到的主鍵設計方案其實基本沒有什麼業務上的意義,它解決的主要問題是讓寫入的資料分佈均勻,從而能合理使用儲存資源,但是這個合理分散式儲存資源卻會給查詢操作帶來極大的問題,甚至有時可以說狹義水平拆分後資料查詢變得困難就是由這種看起來合理的主鍵設計方案所致。
我們還是以實時交易表的實體來說明問題,一個電商平臺下會接入很多不同的商戶,但是不同的商戶每天產生的交易量是不同,也就是說商戶的維度會讓我們使交易資料變得嚴重的不均衡,可能電商平臺下不到5%的商戶完成了全天交易量的80%,而其他95%的商戶僅僅完成20%的交易量,但是作為業務系統的資料表,進行讀操作首先被限制和約束的條件就是商戶號,如果要為我們設計的實時交易表進行狹義的水平拆分,做拆分前我們要明確這個拆分是由交易量大的少量商戶所致,而不是全部的商戶所致的。如果按照均勻分佈主鍵的設計方案,不加商戶區分的分佈資料,那麼就會發生產生少量交易資料的商戶的查詢行為也要承受交易量大的商戶資料的影響,而能產生大量交易資料的商戶也沒有因為自己的貢獻度而得到應有的高階服務,碰到這個問題其實非常好解決,就是在做狹義水平拆分前,我們先做一次廣義的水平拆分,把交易量大的商戶交易和交易量小的商戶交易拆分出來,交易量小的商戶用一張表記錄,這樣交易量小的商戶也會很happy的查詢出需要的資料,心裡也是美滋滋的。接下來我們就要對交易量大的商戶的交易表開始做狹義的水平拆分了,為這些重點商戶做專門的定製化服務。
做狹義水平拆分前,我們有個問題需要過一下,在狹義水平拆分前我們需要先做一下廣義的水平拆分嗎?這個我這裡不好說,具體要看實際的業務場景,但是針對我列舉的實時交易的例子而言,我覺得沒那個必要,因此拆分出的重點商戶交易量本來就很大,每個都在挑戰資料庫讀能力的極限,更重要的是實時交易資料的時間粒度已經很小了,再去做廣義水平拆分難度很大,而且很難做好,所以這個時候我們還是直接使用狹義的水平拆分。拆分完畢後我們就要解決查詢問題了。
做實時查詢的標準做法就是分頁查詢了,在講述如何解決分頁查詢前,我們看看我們在淘寶裡搜尋【衣服】這個條件的分頁情況,如下圖所示:
我們看到一共才100頁,淘寶上衣服的商品最多了,居然搜尋出來的總頁數只有100頁,這是不是在挑戰我們的常識啊,淘寶的這個做法也給我們在實現水平拆分後如何做分頁查詢一種啟迪。要說明這個啟迪前我們首先要看看傳統的分頁是如何做的,傳統分頁的做法是首先使用select count(1) form table這樣的陳述句查詢出需要查詢資料的總數,然後再根據每頁顯示的記錄條數,查詢出需要顯示的記錄,然後頁面根據記錄總數,每頁的條數,和查詢的結果來完成分頁查詢。回到我們的交易表實體裡,有一個重要商戶在做實時交易查詢,可是這個時候該商戶已經產生了1千萬筆交易了,假如每頁顯示10條,記錄那麼我們就要分成100萬頁,這要是真顯示在頁面上,絕對能讓我們這些開發人員像哥倫布發現新大陸那樣驚奇,反正我見過的最多分頁也就是200多頁,還是在百度搜索發現的。其實當資料庫一張表的資料量非常大的時候,select的count查詢效率就非常低下,這個查詢有時也會近似個全表檢索,所以count查詢還沒結束我們就會失去等待結果的耐心了,更不要是說等把資料查詢出來了,所以這個時候我們可以學習下淘寶的做法,當商戶第一次查詢我們準許他查詢有限的資料。
我自己所做的一個專案的做法就是這樣的,當某個商戶的交易量實在是很大時候我們其實不會計算資料的總筆數,而是一次性查詢出1000條資料,這1000條資料查詢出來後存入到快取裡,頁面則只分100頁,當使用者一定要查詢100頁後的資料,我們再去追加查詢,不過實踐下來,商戶基本很少會查詢100頁後的資料,常常看了5,6頁就會停止查詢了。不過商戶也時常會有查詢全部資料的需求,但是商戶有這種需求的目的也不是想在分頁查詢裡看的,一般都是為了比對資料使用的,這個時候我們一般是提供一個發起下載查詢全部交易的功能頁面,商戶根據自己的條件先發起這樣的需求,然後我們系統會在後臺單獨起個執行緒查詢出全部資料,生成一個固定格式的檔案,最後透過一些有效手段通知商戶資料生成好了,讓商戶下載檔案即可。
對於進行了狹義水平拆分的表做分頁查詢我們通常都不會是全表查詢,而是抽取全域性的資料的一部分結果呈現給使用者,這個做法其實和很多市場調查的方式類似,市場調查我們通常是找一些樣本採集相關資料,透過分析這些樣本資料推匯出全域性的一個發展趨勢,那麼這些樣本選擇的合理性就和最終的結論有很大關係,回到狹義水平拆分的表做分頁查詢,我們為了及時滿足使用者需求,我們只是取出了全部資料中的一部分,但是這一部分資料是否滿足使用者的需求,這個問題是很有學問的,如果是交易表,我們往往是按時間先後順序查詢部分資料,所以這裡其實使用到了一個時間的維度,其他業務的表可能這個維度會不一樣,但肯定是有個維度約束我們到底傳回那些部分的資料。這個維度可以用一個專有的名詞指代那就是排序,具體點就是要那個欄位進行升序還是降序查詢,看到這裡肯定有人會有異議,那就是這種抽樣式的查詢,肯定會導致查詢的命中率的問題,即查出來的資料不一定全部都是我們要的,其實要想讓資料排序正確,最好就是做全量排序,但是一到全量排序那就是全表查詢,做海量資料的全表排序查詢對於分頁這種場景是無法完成的。回到淘寶的例子,我們相信淘寶肯定沒有傳回全部資料,而是抽取了部分資料分頁,也就是淘寶查詢時候加入了維度,每個淘寶的店家都希望自己的商戶放在搜尋的前列,那麼淘寶就可以讓商家掏錢,付了錢以後淘寶改變下商家在這個維度裡的權重,那麼商家的商品就可以排名靠前了。
狹義水平拆分的本身對排序也有很大的影響,水平拆分後我們一個分頁查詢可能要從不同資料庫不同的物理表裡去取資料,單表下我們可以先透過資料庫的排序演演算法得到一定的資料,但是區域性的排序到了全域性可能就不正確了,這個又該怎麼辦了?其實由上面內容我們可以知道要滿足對海量資料的所有查詢限制是非常難的,時常是根本就無法滿足,我們只能做到儘量多滿足些查詢限制,也就是海量查詢只能做到儘量接近查詢限制的條件,而很難完全滿足,這個時候我前面提到的主鍵分佈方案就能起到作用了,我們在設計狹義水平拆分表主鍵分佈時候是儘量保持資料分佈均衡,那麼如果我們查詢要從多張不同物理表裡取的時候,例如我們要查1000條資料,而狹義水平拆分出了兩個物理資料庫,那麼我們就可以每個資料庫查詢500條,然後在服務層歸併成1000條資料,在服務層排序,這種場景下如果我們的主鍵設計時候還包含點業務意義,那麼這個排序的精確度就會得到很大提升。假如使用者對排序不敏感,那就更好做了,分頁時候如果每頁規定顯示10條,我們可以把10條資料平均分配給兩個資料庫,也就是顯示10條A庫的資料,再顯示5條B庫的資料。
看到這裡有些細心的朋友可能還會有疑問,那就是居然排序是分頁查詢的痛點,那麼我們可以不用資料庫查詢,而使用搜索技術啊,NoSql資料庫啊,的確這些技術可以更好的解決分頁問題,但是關係資料庫過渡到搜尋引擎和NoSql資料庫首先需要我們轉化資料,而狹義的水平拆分的資料表本身資料量很大,這個轉化過程我們是沒法快速完成的,如果我們對延時容忍度那麼高,其實我們就沒必要去做資料庫的狹義水平拆分了。這個問題反過來說明瞭使用狹義拆分資料表的業務場景,那就是:針對資料量很大的表同時該表的讀寫的關聯性是沒法有效拆分的。
最後我要講的是,如果系統到了狹義水平拆分都沒法解決時候,我們就要拋棄傳統的關係資料方案了,將該業務全部使用NoSql資料庫解決或者像很多大型網際網路公司那樣,改寫開源的mysql資料庫。文章寫道這裡,我還是想說一個觀點,如果一個系統有很強烈需求去做狹義的水平拆分,那麼這個公司的某個業務那肯定是非常的大了,所以啊,這個方案以公司為單位應該有點小眾了。
好了,今天寫到這裡,祝大家晚安,生活愉快。
來自:夏天的森林
連結:http://blog.jobbole.com/84073/