利用多線程解決select...xx in(...)的性能問題

一、前言

 

      已經有好久沒有更博了,上次也不知道是什麼時候了,還是那句話,寫博客就是爲了記錄,點點滴滴,點到爲止!

      最近一直在忙着組內產品上線發佈和線下測試的事情,還有一系列的產品代碼封版的事情,總之忙碌的時間過的那是相當的快;

      自己也一直想抽點時間寫寫心得體會,當然是關於產品的;但無奈我都勸住了自己,因爲還沒有到合適的時間,我要確保自己在狀態最佳的時間點,大腦能夠完全空下來的時候,再做長篇大論;

      切入本篇正題的最後一句話:“做項目容易,做產品難啊,做核心產品更難啊!”

      放一張產品的其中一個功能截圖, 激勵下自己,相信往後的路,一切都會越來越好!    

      

 

 

 

 

二、場景

  

      回到標題,相信大家在做批量查詢時,一定用到過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

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章