前言
最近遇到個需求,需要定時任務(wù)處理大批量數(shù)據(jù)的導(dǎo)入、更新和刪除,數(shù)據(jù)量每次在十萬級左右,極少情況有百萬、千萬級,所以肯定不能直接一次性處理,內(nèi)存和數(shù)據(jù)庫都頂不住。因此我用lambda表達(dá)式寫了一個鏈?zhǔn)教幚淼姆椒ㄈ缦隆?/p>
private void batchHandle(int total, Function<Integer, List<?>> queryFunction, Function<List<?>, Void> handleFunction) {
for (int i = 0; i < total; i++) {
queryFunction.andThen(handleFunction).apply(i);
}
}
簡介
方法一共有三個參數(shù),第一個參數(shù)total是本次任務(wù)需要處理的數(shù)據(jù)總量,我會先用count的sql查出來,然后傳遞給第一個function作為入?yún)?,它會返回一個List<?>,結(jié)合方法里的for,這里就能拿到分批后的單批量數(shù)據(jù),然后將list傳給第二個function,對單批數(shù)據(jù)做統(tǒng)一的處理邏輯(增刪改查)。
踩坑
- 最初我的設(shè)想是每批2000條數(shù)據(jù),直接拿到數(shù)據(jù)然后操作入庫,寫出來的第一版代碼在我小批量的測試數(shù)據(jù)下也沒出現(xiàn)問題,但是一旦數(shù)據(jù)量上升,這里的單次耗時就會成倍累加,尤其是數(shù)據(jù)量到了千萬級別,哪怕處理邏輯非常簡單,單次耗時1秒總計也有83分鐘,而且這段時間一直占用著資源還是非??鋸埖摹?/li>
- 況且第一個function批量處理肯定需要對全部數(shù)據(jù)進(jìn)行分頁,那么隨著limit數(shù)字越來越大,分頁查詢的耗時會越來越多,并且業(yè)務(wù)表里主鍵是uuid且沒有合適的排序字段。
- 再其次,兩個function里和數(shù)據(jù)庫交互的部分需要做緩存,否則每一批都需要重新查詢,對數(shù)據(jù)庫會造成非常多重復(fù)不必要的訪問。
- 也因為這幾個問題沒考慮周全,導(dǎo)致第一版寫出的定時任務(wù)在測試時非常的慢,最開始單批執(zhí)行耗時3秒,到后來單批耗時就要1分鐘(分頁越來越多),我就直接停止測試開始優(yōu)化了。
優(yōu)化
針對分頁的優(yōu)化。如上所述,如果主鍵是自增或者表里存在某個字段比如時間,那么我們可以通過分批后添加條件截取數(shù)據(jù)的辦法優(yōu)化,但是我的情況并不允許。除此自外還有一個經(jīng)典的優(yōu)化方案,分頁時只查詢主鍵id,再在第二個function里重新查表得到對象list。這樣看起來是多繞了一點路,但實際上mysql主鍵查詢是非??斓模案采w索引”就是這么個意思,查詢條件與結(jié)果都在一個“索引表”中,所以速度快。優(yōu)化后的分頁耗時都在1秒內(nèi)。
針對緩存的優(yōu)化。我在本地做了HashMap緩存(這里遇到了上篇文章的坑,和concurrentHashMap的坑),把能復(fù)用的數(shù)據(jù)都存放在本地,用空間換時間,減少重復(fù)的mysql交互,這部分主要是業(yè)務(wù)相關(guān)的優(yōu)化。
結(jié)果
最終任務(wù)的運行時時間獲得了巨大優(yōu)化,百萬級數(shù)據(jù)的處理花了4分鐘,日常十萬級數(shù)據(jù)只要1分鐘不到,已經(jīng)在可接受范圍內(nèi)了。