有幸在兩家不同的公司不同程度的參與了涉及分表的項目课蔬,其背景烁峭、分表方式容客、結果均有不同秕铛,根據(jù)自己的實際感受進行了簡單的總結歸納约郁,形成此文,如有不正之處但两,歡迎各位看客指正
先說幾點自己的總結
1.為什么要分表? 及分表時機
我們都知道鬓梅,當表數(shù)據(jù)量達到一定程度后,就會有慢查出現(xiàn)谨湘。根據(jù)自己對項目的感受和觀察來看绽快,當一個表的字段不算特別多,且索引合理紧阔,大部分sql合理(少回表坊罢、盡可能不聯(lián)表或少聯(lián)表等等)的情況下,一個表的數(shù)據(jù)量可能到2000W到3000W左右就可能需要考慮分表了擅耽,到了5000W上下活孩,分表可能就已經(jīng)到了迫不及待的地步了。不過具體這個區(qū)間乖仇,也是根據(jù)自己的感受估計的憾儒,實際上參與的兩次分表均達到了5000W以上的數(shù)據(jù)量询兴,甚至第一次進行分表時數(shù)據(jù)量已經(jīng)達到三四億(除了用id查,其他的條件查詢似乎都查不動的地步)
2.分表的方式起趾?
一般情況下诗舰,有根據(jù)業(yè)務參數(shù)(例如用戶id或其他業(yè)務id)哈希或者取模的方式來分表,或者根據(jù)時間來分表例如年训裆、月眶根、周,恰好自己這兩次分表一次是按用戶id取模分表的边琉,另一次是按月分表的汛闸。具體如何選擇,感覺并無絕對的對錯艺骂,按自己總結來看主要需要考慮以下幾點:
- 分表后數(shù)據(jù)能較為均勻的分散到分表中
- 便于兼容查詢
- 從業(yè)務的角度來思考清晰合理诸老,便于以后業(yè)務擴展
- 盡量減少需要跨表merge
- 如果能通過分表來把數(shù)據(jù)的冷熱區(qū)分開就更好了
- 數(shù)據(jù)維護成本
其實兩次分表的操作在分表方式的選擇上,都是經(jīng)過一定考慮的钳恕,但后來也確實發(fā)現(xiàn)遇到了一些問題别伏,后文詳細贅述
3.關于分表后上線,及數(shù)據(jù)遷移的問題
感覺自己兩次分表經(jīng)歷在這塊都算是做的比較好的忧额,幾乎都做到了不停服或盡量少停服上線(第一次停了約1小時厘肮,第二次用戶完全無感,主要是追數(shù)據(jù)的方式不同導致的睦番,后文贅述具體情況)类茂,就是在數(shù)據(jù)字典中增加了業(yè)務開關(配置中心直接修改,不需要發(fā)版)托嚣,主要是讀開關和寫開關巩检,實話講,第一次這個設計時是有人指導的示启,第二次感覺這個方式不錯兢哭,就直接沿用保留了相關開關設計,整體的上線流程如下:
1.先寫好查分表的程序夫嗓,是一個單獨的邏輯策略類迟螺,原來查大表的是另一個策略類,策略工廠中依據(jù)讀開關和寫開關來控制走哪套策略舍咖。
2.上線時默認雙寫矩父,讀老的大表,相當于查詢完全沒改排霉,只是數(shù)據(jù)寫入的時候多寫了一份
3.等待分表的數(shù)據(jù)和老表完全一致后窍株,切換讀開關,改成讀新的分表,這樣一旦有問題還能隨時切回老表夹姥,方便快速回滾
4.穩(wěn)定一段時間后(看具體情況杉武,可以一周,也可以一個月)辙售,若老的大表確實沒有維護需求了轻抱,可將寫開關改為只寫新的分表,并在下次版本發(fā)布時旦部,將讀開關和寫開關的邏輯下掉祈搜。
4.關于id發(fā)號器
最好是有單獨的id發(fā)號器,或者簡單點就用一個自增長的只有一個主鍵的數(shù)據(jù)庫表來做id發(fā)號器(實際兩次分表都是這么玩的)士八,如果是雪花算法或者其他的方式容燕,感覺要么id不是單調(diào)的,要么過于復雜了(個人感覺)
5.關于無法通過分表字段作為條件的查詢
個人想法還是盡量在選擇分表方式的時候能夠覆蓋C端的絕大多數(shù)查詢婚度,運營后臺或者其他偏B端的服務蘸秘,可以通過構建ES寬表等方式來查詢,實在無法覆蓋的蝗茁,可以考慮根據(jù)不同的業(yè)務參數(shù)分多套表醋虏。
實在不行的,再去考慮異構索引表哮翘,不是不能解決問題颈嚼,而是一旦異構索引表查出需要跨多張表去merge數(shù)據(jù)時,性能極差饭寺。這塊感覺也回到第二點分表方案的設計時需要考慮盡量減少跨表merge的問題了
具體回顧一下自己的兩次分表實踐
一.根據(jù)業(yè)務id(uid)取模分表
1.1經(jīng)歷概述
事項 | 具體方式/情況 | 解釋備注 |
---|---|---|
業(yè)務背景 | 在線教育班課業(yè)務阻课,用戶課次表 | 用戶報班后,具體用戶每一節(jié)課的數(shù)據(jù)艰匙,冗余數(shù)據(jù)便于快速查詢限煞,避免查一次需要經(jīng)過多張表,且用戶報班后如果經(jīng)過調(diào)/退/換課有該表更好操作一些 |
分表前數(shù)據(jù)量 | 約4億 | |
分表策略 | 根據(jù)用戶id取模分表 | C端用戶查詢旬薯,基本都是學生基于uid查詢上課的課次 |
分表數(shù) | 256張 | shardingValue.getValue() % 256 |
分表框架 | sharding-jdbc | |
潛在問題 | 部分查詢不是通過uid來查詢的 | 1.老師查的場景; 2.根據(jù)產(chǎn)品或班級查詢該班的學員時; |
解決方式 | 易購索引表 | 將不帶uid的查詢條件晰骑,冗余一份和uid關聯(lián)異構索引表适秩,易購索引表當時還是采用的mysql表绊序,且由于數(shù)據(jù)量也很大,故也進行了分表秽荞,分32張 |
數(shù)據(jù)遷移 | 由于原表需要分256張骤公,易購索引表也需要分32張,且數(shù)據(jù)量較大扬跋,經(jīng)和DBA討論后阶捆,是由DBA寫的腳本程序來遷移同步 | 1.臨近上線時設定一個備份時間,將該備份時間之前的數(shù)據(jù),通過腳本初始化到新的兩套分表中 2.備份時間后的數(shù)據(jù)洒试,通過監(jiān)聽binlog時間追增量數(shù)據(jù) |
上線方式 | 1.新的分表時獨立的項目去讀寫倍奢,原項目操作老表的地方,增加讀寫開關來切換操作的是老表還是新表 2.對新表的操作方式統(tǒng)一按發(fā)消息垒棋,或者調(diào)用新項目接口的方式來操作 3. 剛上線時業(yè)務開關切換寫老卒煞,讀老 4.執(zhí)行DBA的初始化腳本,初始化某備份時間前的數(shù)據(jù)到新的分表中 5.停服叼架,執(zhí)行DBA追增量數(shù)據(jù)的腳本畔裕,回溯從備份時間節(jié)點到停服的時間點中的binlog事件到新的分表中 6.一致后,開關切換到雙寫讀老表乖订,重啟服務 7.程序寫驗證無問題后扮饶,開關切換雙寫讀新 8.回歸驗證無問題,且確認新服務穩(wěn)定后乍构,開關切換寫新讀新 9.下次上線時下線開關邏輯代碼 |
由于是通過回溯binlog事件的方式追數(shù)據(jù)甜无,故在追增量數(shù)據(jù)時只能停服,否則一直會產(chǎn)生新的binlog事件 |
分表結果 | 1.分表前基本除id外已經(jīng)完全查不動了哥遮;分表后對于通過單個uid的查詢能快速查詢 2. 非uid查詢條件的毫蚓,在經(jīng)過易購索引表拿到uid后,也能正常查詢昔善,但一旦拿到的uid分散在N個分表中元潘,存在較大性能問題 3.批量uid查詢?nèi)绻稚⒃诙鄠€分表中也存在較大性能問題 4.由于八成以上的場景確實是根據(jù)單個uid的查詢箫攀,故一定程度上也解決或者緩解了最初完全查不動的問題川尖,但總體來講只能說勉強完成了最初期望的五六成吧 5.后續(xù)進一步分析業(yè)務后,從需求和業(yè)務邏輯上優(yōu)化江咳,取消了用戶課次表的維護 |
遇到的關鍵問題在于返咱,sharding-jdbc的多表路由和merge時存在較大的性能隱患钥庇,這點是一開始沒意識和考慮到的,這塊的相關資料例如這篇文章https://blog.csdn.net/D_19901719576/article/details/102925945 |
參與角色/程度 | 1.在他人的指導下咖摹,參與方案設計時的討論 2.參與實際開發(fā)落地评姨,加上自己共計三個開發(fā),我和另一個開發(fā)新寫的分表項目和策略萤晴,還有一個開發(fā)改造的業(yè)務代碼調(diào)用處(新增業(yè)務開關切換) |
自己是首次參與到分表項目中吐句,方案設計時還算踴躍,應該說會有一些自己的想法店读,但由于不確定正確性嗦枢,故僅討論時偶爾提出,但最終還是按大家一致的意見落地屯断,總體來講還算是不錯的經(jīng)歷 |
1.2后續(xù)自己的總結與思考
1.關于課次表的合理性的一些思考:
1.1 用戶課次表的冗余比較方便的操作用戶調(diào)課/換課/退課等文虏,但確實會導致用戶的課次數(shù)據(jù)一直增加侣诺,表會膨脹,且該表的數(shù)據(jù)可以由多個小表組合得出氧秘,故當時的分析后續(xù)是取消了該表的維護年鸳,直接從業(yè)務和邏輯上改成了多個小表拼接出大表結果;
1.2 但用戶課次數(shù)據(jù)丸相,尤其是上過的課阻星,是否應該記錄下來,自己始終存在疑惑已添。
2.如果保留用戶上課的數(shù)據(jù)妥箕,是否有其他的分表方式?
2.1大學里面學校上課時更舞,也存在每個人選的課不一樣畦幢,一個老師上課同時面對多個不同的班級(子班)的情況,但一個比較大的區(qū)別在于缆蝉,大學的課都是一個學期一個學期宇葱。如果按學季分表是否能更有效的支持后續(xù)的業(yè)務?未進行驗證刊头,且單個學季的數(shù)據(jù)量會不會也很大黍瞧,此點存疑。
2.2當時的課程類型分為小原杂、初印颤、高,及特定的課程類型(講座/免費課)等穿肄,如果按具體類型分表是否能更有效的支持后續(xù)的業(yè)務年局?未進行驗證,此點存疑咸产。
2.3針對不同的業(yè)務入?yún)⑹阜瘢床煌姆绞椒直恚∠悩嬎饕砟砸纾缋蠋焷聿橹苯影磘eacher_id分表僵朗,學生來查直接按uid分表?未進行驗證屑彻,此點存疑验庙。
二.按月分表
2.1經(jīng)歷概述
事項 | 具體方式/情況 | 解釋備注 |
---|---|---|
業(yè)務背景 | 游戲互娛業(yè)務,用戶背包道具表/用戶道具消耗表 | 按用戶和道具記錄的流水酱酬,同一道具多次獲得是多條數(shù)據(jù)壶谒,使用/兌換也只改用的那條數(shù)據(jù)的狀態(tài) |
分表前數(shù)據(jù)量 | 約5000萬 | |
分表策略 | 1.根據(jù)數(shù)據(jù)產(chǎn)生時間,按月分表 2.根據(jù)用戶id,道具id膳沽,單獨異步維護一份統(tǒng)計表,來支持查總價值,總數(shù)等 |
1.流水表主要是按時間段或者用戶最近的獲得記錄等信息時需要用到 2.統(tǒng)計信息避免直接在流水表中做統(tǒng)計挑社,消息異步維護統(tǒng)計表陨界,直接查統(tǒng)計表就可以了 3.歷史數(shù)據(jù)實際用戶側很少會查詢,一定程度上按月分表有利于把存量不用的數(shù)據(jù)隔離開痛阻,查詢較多的近幾個月的數(shù)據(jù)相對獨立 |
分表數(shù) | 每月一張,表名為XXXX_YYYYMM | |
分表框架 | 自己邏輯寫的查詢方法和路由方法 | |
潛在問題 | 用戶道具記錄持續(xù)往前翻的時候有可能從當前的表查到了上一頁的表 | 數(shù)據(jù)拼接時容易出問題菌瘪,但并非完全無法解決 |
數(shù)據(jù)遷移 | 提前建好歷史到今年年底的所有分表 上線后定時任務按id從1到最新的逐步處理數(shù)據(jù),每處理500條更新一下已處理的游標阱当,每次都是從已處理游標后拿500條數(shù)據(jù)寫到分表中 |
定時任務的入?yún)⒔刂筰d作為參數(shù)俏扩,這樣通過更改游標和定時任務截止的id,未來也可以隨時通過該任務補償數(shù)據(jù) |
上線方式 | 1.新的分表是獨立的策略去讀寫弊添,原項目操作老表的地方录淡,增加讀寫開關來切換操作的是老表還是新表 2. 剛上線時業(yè)務開關切換雙寫,讀老 3.將新表開始寫的id作為截止id的入?yún)⒂桶樱_始執(zhí)行同步數(shù)據(jù)的任務 4.等定時任務完全同步一致后嫉戚,切換開關讀新 5.持續(xù)觀察穩(wěn)定后,下次上線時下線開關邏輯代碼 |
|
分表結果 | 1.分表前部分查詢出現(xiàn)慢查澈圈;分表后同樣的業(yè)務功能查詢性能較好 2. 未停服彬檀,用戶基本無感 |
|
參與角色/程度 | 由于該項目是個老項目,需要優(yōu)化的大表多達十來個瞬女,討論時多人一起討論窍帝,最終幾個人分別拿了不同的表去分析各自的落地方案,具體到該表的分表基本是自己完整設計和落地 | 有了此前的分表經(jīng)歷后诽偷,對方案的部分細節(jié)設計時更為篤定盯桦,偶爾遇到拿不準的也會提出來和同事討論,最終基本由自己完整落地渤刃,也是一個很珍貴的精力 |