轉載入職第一天,老板竟讓我優化5億數據量,要涼涼?
>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億數據量的定時炸彈了。