>jsoncat:https://github.com/Snailclimb/jsoncat (仿 Spring Boot 但不同於 Spring Boot 的一個輕量級的 HTTP 框架)
前段時間hellohello-tom離職了,因為個人原因,在修整一段時間後,重新入職了一家新公司。入職的第一天tom哥就經歷了一次生產事故,運維同學告警說線上MYSQL負載壓力大,直接就把主庫MYSQL壓崩了(第一天這可不是好兆頭),運維同學緊急進行了主從切換,在事後尋找導致生產事故的原因時,排查到是慢查詢導致mysql雪崩的主要原因,在匯出慢查詢的sql後,專案經理直接說吧這個mysql優化的功能交給新來的tom哥吧,tom哥趕緊開啟跳板機進行檢視,不看不知道一看嚇一跳
單表的資料量已經達到了5億級別!,這尼瑪肯定是歷史問題一直堆積到現在才導致的啊,專案經理直接就把這個坑甩給了tom哥,tom哥心中想,我難道試崗期都過不了麼???
好在tom哥身經百戰,趕快與專案經理與老同事進行溝通,瞭解業務場景,才發現導致現在的情況是這樣的,tom哥所在的公司是主要做IM社交系統的,這個5億級別的資料表是關注表,也是俗稱的粉絲表,在類似與某些大V、或者是網紅,粉絲過百萬是非常常見的。在A關注B後會產生一條記錄,B關注A時也會產生一條記錄,時間積累久了才達到今天這樣的資料規模,專案經理慢悠悠的對tom哥說,這個優化不用著急,先出方案吧!tom哥心中一萬個草泥馬經過,這上來就給了一塊不好啃的骨頭,看來是要試試我能力的深淺啊。
按照tom哥之前經驗,單表在達到500W左右的資料就應該考慮分表了,常見分表方案無非就是hash取模,或者range分割槽這兩種方法,但是這次的資料分表與遷移過程難度在於兩方面:
1、資料平滑過度,在不停機的情況把單表資料逐步遷移(老闆說:敢當機分分鐘損失幾千塊,KPI直接給你扣成負的)
2、資料分割槽,採用hash還是range?(暫時不能使用一些分庫分表中介軟體,無奈。)
首先說說hash
常規我們都是拿使用者id進行取模,模到多少直接把資料塞進去就行了,簡單粗暴,但是假如說user_id=128與user_id=257再模128後都是對應user_attention_1這個表,他倆也恰好是網紅,旗下粉絲過百萬,那輕輕鬆鬆兩個人就能把資料表撐滿,其他使用者再進來資料的時候無疑user_attention_1這個表還會成為一張大表,這就是典型的資料熱點問題,這個方案可以PASS,有的同學說可以user_id和fans_id組合進行取模進行分配,tom哥也考慮過這個問題,雖然這樣子資料分配均勻了,但是會有一個致命的問題就是查詢問題(因為目前沒有做類似mongodb與db2這種高效能查詢DB,也沒做資料同步,考慮到工作量還是查詢現有的分表內的資料),例如業務場景經常用到的查詢就是我關注了那些人,那些人關注了我,所以我們的查詢程式碼可能會是這樣寫的
//我關注了誰
select * from user_attention where user_id = #{userId}
//誰關注了我
select * from user_attention where fans_id = #{userId}
在我們進行user_id與fans_id組合後hash後,如果我想查詢我關注的人與誰關注我的時候,那我將檢索128張表才能得到結果,這個也太噁心了,肯定不可取,並且考慮到以後擴容至少也要影響一半資料,實在不好用,這個方案PASS。
接下來說說range
Range看起來也很簡單,使用者id在一定的範圍時候就把他路由到一個表中,例如使用者id=128,那就在[0,10000]這個區間中對應的是user_attention_0這個表,就直接把資料塞進去就可以了,但是這樣同樣也會產生熱點資料問題,看來簡單的水平分割槽已經不能滿足,這個方案也可以pass了,還是要另尋他經啊。
經過tom哥日夜奮戰,深思熟慮之後,給出了三個解決方案
先說說第一種方案range+一致性hash環組合(hash環節點10000)
什麼是hash環看這裡

想採用這個方案主要是因為
1、擴容簡單,影響範圍小,只涉及hash環上單個節點影響
2、資料遷移簡單,每次擴容只需吧新增的節點與後置節點進行資料互動
3、查詢範圍小,按照range與hash關係檢索部分表分割槽
大概思路我們還是先按照user_id進行大概範圍劃分,但是range之後我後面對應的可能就不是一個表了,而是一個hash環
在每個range區域後都對應著自己一套的環,我們可以根據實際情況進行擴容,比如在[1,10000]這個範圍內只有2個大V,那我們分三個表就夠了,預留1500萬的資料容量。[10001,20000]中有4個網紅和大V,hash環上就給出實際4張表,我們的使用者id可以順時針順序坐落到第一個物理表,資料進行入庫。
凡事有利有弊,方案也要結合工時,實際可行性與技術評審之後才能決定,弊端我們也要列出來
1、設計複雜,需要增加range區域與hash環關係
2、系統內修改波及較多,查詢關係複雜,多了一層路由表的概念,雖然儘量吧使用者資料分配到一個區之內,但是想查詢誰關注我,與我關注誰這樣的邏輯時還是複雜。
說說第二種方案range+hash取模(hash模300)
這個其實就比較好理解了,就是一個簡單的range+hash取模組合的形式,先range到一定的範圍後,在這個範圍內進行hash取模找到對應的表進行儲存,這個方案比方案一簡單點,但是方案一存在的問題他也存在,並且他還有擴容資料影響範圍廣的問題。但是實現起來就簡單不少,從查詢方面看根據不同場景可以控制取模的大小範圍,根據實際情況每個分割槽的hash模採用不同的值。
最後一種方案range userId分割槽
這個方案是tom哥覺得靠譜性與實施性可能最高的一種,看起來挺像第二種方案的,但是更具體了一點,首先會定義一箇中間關係表user_attention_routing
欄位名 | 備註 |
---|---|
id | 主鍵id |
user_id_min | 使用者id最小區間 |
user_id_max | 使用者id最大區間 |
table_name | 路由到的表名稱 |
我們會把使用者範圍與路由到哪個表做成關係,根據範圍區間進行查詢,結合現有資料當某個大V,或者網紅資料量比較大,我們就給他路由自成一表資料大概是這樣的
user_id_min | user_id_max | tablename |
---|---|---|
1 | 10000 | user_attention_0 |
256 | 256 | user_attention_256 |
例如user_id=256是個大V,就把他單獨提出來讓他自成一表,在查詢範圍的時候優先查是否有自己單獨對應的路由表,而其他那些零碎使用者還是路由到一個統一表內,這時候有的同學會說這樣子資料不都又不均勻了麼,tom哥也曾這樣認為,但是分到絕對的均勻基本不太可能,只能做到相對,儘量把某些大V分出去,不佔用公共資源,當某個人突然成為大V後,在吧這個人再單獨分出去,不斷演變這個過程,保證資料的平衡,並且這樣子處理之後很多原來的關聯查詢其實改動不大了,只要在資料遷移後對原來的所有包含user_attention 進行動態的改造即可(使用個mybatis的攔截器就能搞定)PS:其實分析實際業務場景大部分的關注資料還是來源於那些零碎使用者的。
分表方案首先就這樣定了,接下來另一個問題就是查詢問題,上文說過很多業務查詢無非就是誰關注了我,我關注了誰這樣的場景,如果繼續使用之前的
//我關注了誰
select * from user_attention where user_id = #{userId}
//誰關注了我
select * from user_attention where fans_id = #{userId}
這樣的方案,當我要查詢我的粉絲有哪些時,這樣就悲劇了,我還是要檢索全表根據fansid找到我所有的粉絲,因為表內只記錄了我關注了誰這樣的資料,考慮到這樣的問題,tom哥決定重新設計資料儲存形式,使用空間換時間的思路,原來處理的方式是使用者在關注對方的時候產生一條記錄,現在處理方式是使用者A在關注使用者B時寫入兩條資料,通過欄位區分關係,假如user_attention表是這樣的
user_id | fans_id | state |
---|---|---|
1 | 2 | 1 |
2 | 1 | 0 |
在使用者1關注2後產生兩條資料,state(1代表我關注了,0代表我被關注了,2代表我們倆互關)
,採用這樣的資料儲存方式後,我所有的查詢都可以從user_id進行出發了,不在逆向去推fans_id這樣的方式,資料庫索引設計上,考慮好user_id、fans_id、state與user_id、state這樣的結構即可,是不是感覺很簡單,雖然資料量儲存變多了,但是查詢方便了好多。
分表和查詢問題解決了,最後就是要考慮資料遷移的過程了,這一步也非常重要。搞不好就要被扣掉自己的KPI了(步步為營啊)
資料遷移最需要考慮的問題就是個時效性,遷移程式必不可少,如何生產環境正常跑著,遷移指令碼線下跑著資料互不影響呢?答案就是經典套路資料雙寫
,因為老的資料不是一下子就遷移到新表內的,現在和user_attention產生的資料還是要保持的,在產生老表資料的同時,根據路由規則,直接存到新表內一份,線下的遷移程式多開幾臺服務慢慢跑唄,不過可要控制好資料量,別佔滿io影響生產環境,線下的模擬和演練也是必不可少的,誰都不能保證會不會出啥問題呢。遷移指令碼和線上做好user_id和fans_id的唯一索引就行,在某些極端情況下,資料會存在新表內寫入資料,但是老表內資料還沒更新的可能這個做好版本號控制和日誌記錄就可以了,這些都比較簡單。
當新表資料和老表完全同步時我們就可以吧所有系統內波及老表查詢的語句都改成新表查詢,驗證下有沒有問題,如果沒有問題最後就可以痛快的
truncate table user_attention;
幹掉這個5億資料量的定時炸彈了。好了今天分享就結束了,看來tom哥不僅能挺過試崗期也能挺過試用期了,不說了下班回家抱娃去了?。
我是hellohello-tom,一個二線城市的程式設計師