這段代碼讓程序執行效率提高200倍,值得一看!

點讚的靚仔,你最帥哦!

源碼已收錄github 查看源碼java

前言

前幾天業務系統部門將咱們數據平臺給投訴了,由於在工做時間內,業務系統查詢不到想要的數據,這種問題可大可小,但畢竟影響到了業務的正常運行,全部的技術都是爲業務服務的,因此不論技術難度大小,必需要進行整改,同時做爲互聯網的‘工匠精神’,咱們不光要讓功能正常運行,還要讓功能以最優的狀態運行。git

系統介紹

整個系統能夠從功能上分爲3塊:github

  1. 業務系統:在上游有不少的業務系統,業務系統的運行產生不少的數據,這些數據分散在不少的數據庫中,大部分是MySQL數據庫
  2. 數據智能平臺:數據智能平臺屬於中臺系統,主要爲業務系統提供強大的數據支撐服務,下層鏈接數倉。
  3. 數據倉庫: 數據倉庫統一集中的管理全部的數據,數倉會將業務系統產生的數據按天進行加工、抽取、轉換到數據倉庫存儲。

當一天結束後,各個業務系統產生了大量的數據,這些數據由定時任務進行加工、抽取到數據倉庫存儲,當半夜你還在睡覺的時候,這些定時任務就在默默的運行着。
sql

而天天加工的數據一般要求在上班工做時間以前加工完成,而後經過數據智能平臺的查詢系統供業務系統查詢調用,這一次數據沒有查詢到是由於在次日早上10點,數據尚未加工完成。下面就是找問題優化了,由於正常來說,即便定時任務鏈再長,也不會慢到次日10點鐘數據尚未出來。下面就是找問題,而後進行優化了。數據庫

任務優化

經過任務日誌發現有一個上游系統的數據抽取執行時間有3個小時,而數據量僅100萬。固然,光憑這樣還沒法肯定這個任務是不是能夠被優化的。緩存

查看任務代碼,邏輯還比較簡單:有一張原始數據表,記錄商品信息以及定義的分類(這一點是虛構的,實際狀況要複雜一些,我這裏精簡而後轉換了一下,便於理解),而數倉的目標表是將分類和商品分別存儲在不一樣的表中,大體結構以下。app

那爲何須要進行這樣的轉換呢? 這是由於整個大的系統,通常來講只能定義一些基本的規範,而具體的細節規範則沒法約束,好比A系統的身份證字段名稱爲card_no,而B系統的身份證字段名稱爲crdt_no(這種狀況你們應該常常遇到);再好比處理實體關係的時候,處理方式也是不一樣的,1對1的關係,能夠建兩張表關聯,也能夠一張表都存儲,這就形成了多個系統的不統一性,而這種狀況是不可避免的,由於從業務系統來講,都保證了系統的正常運行。測試

而數倉對多個原始數據處理的時候就須要考慮到兼容的問題,因此就會出現如上圖的轉換過程。優化

而這個任務執行3個小時的緣由在於原始表中的一條記錄,會轉換到數倉表中的三張表中,並且這三張表是經過id進行關聯,整個代碼流程以下。spa

然而問題來了,100萬的數據,跑了3個小時,而後我開始嘗試去優化程序的執行流程,大概從一下幾點入手

  1. 將分類緩存,分類在系統中已經固定,不會發生變化,緩存能夠減小查詢數據庫的次數
  2. 每次從原表中讀取的數據更多,從原來的500/次 -> 2000/次

通過優化,效率有一些提高,但並非很明顯(有同窗可能要問了,這些都是很基本的,爲何最開始作? 咳咳。。。這個嘛,歷史緣由吧,在最開始數據可能很少,不論以什麼方式執行,都差異不大,好比執行10分鐘和執行20分鐘,看似2倍的執行效率,可是因爲沒有影響到業務系統,且一直正常運行,也就沒有看出問題)。

這裏數據是須要關聯的,因此咱們是須要插入數據並拿到這條記錄的自增加id,而後插入到關聯表,而表結構基本不可能去動的(表結構動了那真是牽一髮而動全身了,次日準得被叫去喝茶)。

那麼咱們先來分析一下這裏爲何執行這麼慢呢。

  1. 原表100萬的數據,每次查詢出2000條,因此查詢的總次數就是1000000/2000 = 500次,這確定消耗不了多少時間。這裏基本沒有優化的空間,就算一次所有查詢出來,也僅僅節省499次的查詢時間(也不可能一次查詢這麼多數據)
  2. 查詢的2000條數據,數據轉換,而後依次插入到信息表以及關聯表中,這裏是一條一條解析執行的,總計插入數據庫4000次,毫無疑問,這裏是最耗時的。數據轉換是必須的,並且是在內存中操做,因此耗時不是特別多;那麼剩下的就是總計100萬 * 2的數據庫插入次數,可否進行優化呢?

首先想到的就是批量插入,批量插入能夠有效的下降數據庫訪問次數。可是這裏不能進行批量插入是由於須要取到自增加id,感受陷入了困境。

當天晚上昨晚運動以後,拋開煩惱,以爲渾身舒坦。

忽然,腦殼靈光一閃,數據庫的自增加id是由數據庫控制的數值,而自增加的步長咱們是知道的,好比自增加步長爲1,當前自增加id爲1的話,那麼能夠確定,下一條記錄的自增加id就爲2,以此類推。

那是否能夠插入一條記錄,取到自增加id,而後就能夠計算出以後全部數據的自增加id,而再也不須要每條記錄都去取自增加id了。

可是這樣也有一個問題,就是在數據轉換導入的過程當中,不能有其餘的程序向表中插入數據,否則會致使程序計算的自增加id匹配不上。而這個問題根本不存在,由於數倉的數據都是由原始表計算插入的,在同一時間是沒有其餘的任務寫這張表,那麼咱們就能夠放心大膽的幹了。

我將這一部分邏輯抽象出來作成了一個demo,並填充了100萬的數據,優化前的核心代碼以下:

private void exportSource(){
    List<Source> sources;
    //刷新日期, 這裏屬性做爲日期, 其實應該以局部變量看成參數傳遞會更好,原諒我偷個懶
    date = new Date();
    int pageNum = 1;
    do{
        sources = sourceService.selectList(pageNum++, pageSize);
        System.out.println(sources);
        for (Source source : sources) {
            //數據轉換
            Target transfer = transfer(source);
            //插入數據,返回自增加id
            targetService.insert(transfer);
            TargetCategory targetCategory = new TargetCategory();
            Category category = allCategory.get(source.getCategoryName());
            if(category != null){
                targetCategory.setCategoryId(category.getId());
            }
            targetCategory.setTargetId(transfer.getId());
            //插入分類數據
            targetCategoryService.insert(targetCategory);
        }
    }while(sources.size() > 0);
}

效率就不說了,我跑了1個小時,差很少跑了20萬的數據(預計總運行時間大於5小時),而後沒繼續跑了,在這個基礎上作了優化。

private void exportSourcev2(){
    List<Source> sources;
    //刷新日期, 這裏屬性做爲日期, 其實應該以局部變量看成參數傳遞會更好,原諒我偷個懶
    date = new Date();
    int pageNum = 1;
    Integer startId = 0;
    do{
        sources = sourceService.selectList(pageNum++, pageSize);
        List<Target> sourceList = new ArrayList();
        List<TargetCategory> targetCategoryList = new ArrayList();
        for (Source source : sources) {
            //數據轉換
            Target transfer = transfer(source);
            //第一次,取出自增加id,後面就直接計算
            if(startId == 0){
                //插入數據,返回自增加id
                targetService.insert(transfer);
                startId = transfer.getId();
            }else{
                startId++;
                sourceList.add(transfer);
            }
            TargetCategory targetCategory = new TargetCategory();
            Category category = allCategory.get(source.getCategoryName());
            if(category != null){
                targetCategory.setCategoryId(category.getId());
            }
            targetCategory.setTargetId(transfer.getId());
            targetCategoryList.add(targetCategory);
        }
        if(sourceList.size() > 0){
            targetService.insertBatch(sourceList);
        }
        if(targetCategoryList.size() > 0){
            targetCategoryService.insertBatch(targetCategoryList);
        }
    }while(sources.size() > 0);
}


從測試結果來看,執行時間已經大大下降,從至少5小時的運行時間縮短到12分鐘不到。

才11分鐘,咱們怎麼就知足了,不夠不夠!!!!

好吧,可憐的博主繼續動動歪腦筋,想個辦法知足各位看官。其實從測試打印的SQL速度就可以感受出來,在剛剛開始的時候,SQL是刷刷刷的打印,到了後面,SQL是刷。。。刷。。。刷的打印,感受像是快沒油了的汽車,從這個地方入手,看可否優化。

public List<Source> selectList(Integer pageNum, Integer pageSize) {
    //分頁查詢
    PageHelper.startPage(pageNum,pageSize);
    List<Source> sources = sourceMapper.selectList();
    return sources;
}
//打印的某條查詢SQL
==>  Preparing: SELECT * FROM source LIMIT ?, ?
==> Parameters: 998000(Long), 2000(Integer)

咱們把這條SQL在Navcat執行下看看時間呢

查詢1條記錄竟然用時1秒,再來看另一個查詢。

用時70ms,這個挺正常的。因爲limit查詢會查詢出offset的全部數據而後將offest以前的數據丟棄,就是進行了全表檢索,因此形成效率低。能夠經過where id 構建條件來優化查詢效率。

@Select(" SELECT  t.* " +
            " FROM    ( " +
            "        SELECT  id " +
            "        FROM    source " +
            "        ORDER BY " +
            "                id " +
            "        LIMIT #{offset}, #{size} " +
            "        ) q " +
            "JOIN    source t " +
            "ON      t.id = q.id")
    List<Source> selectList(PageParam page);


這樣一來,查詢效率也獲得了優化。

從實測結果,總體效率提高一倍還多,由12分鐘提高至5分鐘。

總結

本文的提高程序執行效率是經過批量插入以及優化分頁查詢效率來實現的,歡迎借鑑、歡迎指正。

相關文章
相關標籤/搜索