一、前言
已經有好久沒有更博了,上次也不知道是什麼時候了,還是那句話,寫博客就是爲了記錄,點點滴滴,點到爲止!
最近一直在忙着組內產品上線發佈和線下測試的事情,還有一系列的產品代碼封版的事情,總之忙碌的時間過的那是相當的快;
自己也一直想抽點時間寫寫心得體會,當然是關於產品的;但無奈我都勸住了自己,因爲還沒有到合適的時間,我要確保自己在狀態最佳的時間點,大腦能夠完全空下來的時候,再做長篇大論;
切入本篇正題的最後一句話:“做項目容易,做產品難啊,做核心產品更難啊!”
放一張產品的其中一個功能截圖, 激勵下自己,相信往後的路,一切都會越來越好!
二、場景
回到標題,相信大家在做批量查詢時,一定用到過in這個條件,可能你會說,我不用in,我用聯合查詢啊,本篇討論的是如果你用in,所以,就不要糾結了;
比如,select * from xxxTable where id in(1,2,3,4......)
比如,select * from xxxTable where name in ('a','b','c','d'......)
如果括號裏面的值少的話,比如50個還好,100個也還好,但是如果是1000個,10000萬個呢,事實上業務系統不會一下子查詢這麼多的,不有分頁控制着呢嗎。但是如果你在sql語句中用了in這個條件,一但你參數的個數沒有控制好,外界傳了一個特別size大的數值過來了,一種情況是,查詢響應的結果會很耗時,因爲查詢的數據量大不說且in本身效率也不高啊,第二個就是in後面跟的參數太長,可能直接在sql編譯這關就被卡死了,idea會無情的告訴你:
Prepared or callable statement has more than 2000 parameter markers
遇到這種情況怎麼辦? 推薦一種方法,分批查,一次查詢很少量,利用多線程併發查最後合併結果返回,以犧牲cpu切換的時間來換取查詢性能上的快感,想必有些人一聽到多線程就頭大,難道就沒有其他辦法了嗎?有啊,你可以從數據表結構本身調整、或者sql語句優化上做文章,當然這兩種比較靈活就不在本篇的討論範圍內了;
多線的好處我就不用多說了吧,當然用多線程也是有風險的好吧,秉承着不浪費new太多線程消耗內存資源,所以,最好用線程池管理着線程,用完的放到池子中等待下次用,不夠的池子在擴,好吧,下面上demo,演示一遍就OK了!
三、多線程創建的方式那麼多,選哪個呢?
多線的創建方式有四種,一種繼承Thread類,一種實現Runable接口,再一種就是實現Callable接口,最後一種是利用線程池啓動多線程;
注意,select查詢是要有結果返回的,二是繼承Thread類和實現Runable接口效果是一樣的,都要複寫run方法,推薦使用實現的方式,因爲繼承在Java中是單線條的,實現是可以多實現的;但是這兩種方式滿足不了帶結果返回的,因爲線程執行完了,我要怎麼拿到執行後的result呢,別急,我們來看下Callable接口是怎麼定義的:
那我就用這種吧,再結合Excutor批量提交任務(第四種方式,創建多線程),最後實現大批量in的高效查詢!
四、項目結構
話不多說,直接看關鍵代碼片段:
@GetMapping("/query")
public ResponseEntity query(){
// 模擬10000個id
List<Long> ids = new ArrayList<>();
for (long i = 0; i <10000 ; i++) {
ids.add(i);
}
// 將ids按多少個進行拆分
List<List<Long>> parts = Lists.partition(ids, 50);
// 有多少份,就有多少次請求,有多少次請求就有多少個task
List<Callable<List<GxTask>>> tasks = new ArrayList<>();
for (List<Long> part : parts) {
GxTaskQuery taskQuery = new GxTaskQuery(taskService);
taskQuery.setIds(part);
tasks.add(taskQuery);
}
// 每一個任務(線程)執行的結果都是一個future對象,這個對象的數據就是List<GxTask>
List<Future<List<GxTask>>> futures = new ArrayList<>();
for (Callable<List<GxTask>> task : tasks) {
// 提交任務 == 注意這時候並沒有觸發線程執行,就緒狀態
futures.add(executor.submit(task));
}
// 最終要查詢的任務集
List<GxTask> allTasks = new ArrayList<>();
// 遍歷執行結果,準備正式執行(get)
for (Future<List<GxTask>> future : futures) {
try {
// 查詢任務超過3s的直接棄掉(cancel取消任務)
List<GxTask> taskList =future.get(3000,TimeUnit.MILLISECONDS );
if(taskList !=null && taskList.size() != 0){
allTasks.addAll(taskList);
}
}catch (InterruptedException | ExecutionException | TimeoutException e){
future.cancel(true);
}
}
return ResponseEntity.ok(allTasks);
}
需要注意的有三點:
(1)ids分段,比如10000個id,每50或100個分一批,這裏利用的是google的guava包
(2)注意excutor提交的任務返回結果是Feature類型
(3)只有調用feature.get(x)方法纔算真正執行任務,且這個執行的結果是可以設置等待時間的,如果超過設置的時間任務還沒有返回結果,那麼這個task就會被取消(中斷cancel),這是必然啊,不能一個查詢拖了全局查詢的後腿吧,當然這種情況是很少會發生的。
五、測試
代碼裏沒有真正去查數據庫,但是模擬了一把in的查詢:
直接項目啓動,postman走一波測試:
測試結果:
六、GitHub項目地址
地址:https://github.com/kobeyk/MultiTask