1. 程式人生 > >(百萬資料量級別)java下的mysql資料庫插入越插越慢的問題解決

(百萬資料量級別)java下的mysql資料庫插入越插越慢的問題解決

http://blog.csdn.net/qq547276542/article/details/75097602

最近的專案需要匯入大量的資料,插入的過程中還需要邊查詢邊插入。插入的資料量在100w左右。一開始覺得100w的資料量不大,於是就插啊插,吃了個飯,回來一看,在插入了50多w條資料後,每秒就只能插10條了。。覺得很奇怪,為啥越插越慢呢?  於是就開始分析插入的時間損耗,想到了如下的解決方案:(mysql使用的INNODB引擎

1.分析是否是由主碼,外碼,索引造成的插入效率降低

        主碼:由於主碼是每張表必須有的,不能刪除。而mysql會對主碼自動建立一個索引,這個索引預設是Btree索引,因此每次插入資料要額外的對Btree進行一次插入。這個額外的插入時間複雜度約為log(n)。這個索引無法刪除,因此無法優化。但是每次插入的時候,由於主碼約束需要檢查主碼是否出現,這又需要log(n),能否減少這個開銷呢?答案是肯定的。我們可以設定主碼為自增id  AUTO_INCREMENT

 ,這樣資料庫裡會自動記錄當前的自增值,保證不會插入重複的主碼,也就避免了主碼的重複性檢查。

        外碼:由於我的專案的插入表中存在外碼,因此每次插入時需要在另一張表檢測外碼存在性。這個約束是與業務邏輯相關的,不能隨便刪除。並且這個時間開銷應當是與另一張表大小成正比的常數,不應當越插入越慢才對。所以排除。

        索引:為了減少Btree插入的時間損耗,我們可以在建表時先不建索引,先將所有的資料插入。之後我們再向表裡新增索引。該方法確實也降低了時間的開銷。

        經過以上的折騰,再進行測試,發現速度快了一點,但是到了50w條後又開始慢了。看來問題的關鍵不在這裡。於是繼續查資料,又發現了個關鍵問題:

2.將單條插入改為批量插入(參考:點選開啟連結

        由於java中的executeUpdate(sql)方法只是執行一條sql操作,就需要呼叫sql裡的各種資源,如果使用for迴圈不停的執行這個方法來插入,無疑是開銷很大的。因此,在mysql提供了一種解決方案:批量插入。 也就是每次的一條sql不直接提交,而是先存在批任務集中,當任務集的大小到了指定閾值後,再將這些sql一起傳送至mysql端。在100w的資料規模中,我將閾值設定為10000,即一次提交10000條sql。最後的結果挺好,插入的速度比之前快了20倍左右。批量插入程式碼如下:

  1. publicstaticvoid insertRelease() {    
  2.         Long begin = new Date().getTime();    
  3.         String sql = "INSERT INTO tb_big_data (count, create_time, random) VALUES (?, SYSDATE(), ?)";    
  4.         try {    
  5.             conn.setAutoCommit(false);    
  6.             PreparedStatement pst = conn.prepareStatement(sql);    
  7.             for (int i = 1; i <= 100; i++) {    
  8.                 for (int k = 1; k <= 10000; k++) {    
  9.                     pst.setLong(1, k * i);    
  10.                     pst.setLong(2, k * i);    
  11.                     pst.addBatch();    
  12.                 }    
  13.                 pst.executeBatch();    
  14.                 conn.commit();    
  15.             }    
  16.             pst.close();    
  17.             conn.close();    
  18.         } catch (SQLException e) {    
  19.             e.printStackTrace();    
  20.         }    
  21.         Long end = new Date().getTime();    
  22.         System.out.println("cast : " + (end - begin) / 1000 + " ms");    
  23.     }    

3.一條UPDATE語句的VALUES後面跟上多條的(?,?,?,?)

        這個方法一開始我覺得和上面的差不多,但是在看了別人做的實驗後,發現利用這個方法改進上面的批量插入,速度能快5倍。後來發現,mysql的匯出sql檔案中,那些插入語句也是這樣寫的。。即UPDATE table_name (a1,a2) VALUES (xx,xx),(xx,xx),(xx,xx)... 。也就是我們需要在後臺自己進行一個字串的拼接,注意由於字串只是不停的往末尾插入,用StringBuffer能夠更快的插入。下面是程式碼:

  1. publicstaticvoid insert() {    
  2.         // 開時時間  
  3.         Long begin = new Date().getTime();    
  4.         // sql字首  
  5.         String prefix = "INSERT INTO tb_big_data (count, create_time, random) VALUES ";    
  6.         try {    
  7.             // 儲存sql字尾  
  8.             StringBuffer suffix = new StringBuffer();    
  9.             // 設定事務為非自動提交  
  10.             conn.setAutoCommit(false);    
  11.             // Statement st = conn.createStatement();  
  12.             // 比起st,pst會更好些  
  13.             PreparedStatement pst = conn.prepareStatement("");    
  14.             // 外層迴圈,總提交事務次數  
  15.             for (int i = 1; i <= 100; i++) {    
  16.                 // 第次提交步長  
  17.                 for (int j = 1; j <= 10000; j++) {    
  18.                     // 構建sql字尾  
  19.                     suffix.append("(" + j * i + ", SYSDATE(), " + i * j    
  20.                             * Math.random() + "),");    
  21.                 }    
  22.                 // 構建完整sql  
  23.                 String sql = prefix + suffix.substring(0, suffix.length() - 1);    
  24.                 // 新增執行sql  
  25.                 pst.addBatch(sql);    
  26.                 // 執行操作  
  27.                 pst.executeBatch();    
  28.                 // 提交事務  
  29.                 conn.commit();    
  30.                 // 清空上一次新增的資料  
  31.                 suffix = new StringBuffer();    
  32.             }    
  33.             // 頭等連線  
  34.             pst.close();    
  35.             conn.close();    
  36.         } catch (SQLException e) {    
  37.             e.printStackTrace();    
  38.         }    
  39.         // 結束時間  
  40.         Long end = new Date().getTime();    
  41.         // 耗時  
  42.         System.out.println("cast : " + (end - begin) / 1000 + " ms");    
  43.     }    

        做了以上的優化後,我發現了一個很蛋疼的問題。雖然一開始的插入速度的確快了幾十倍,但是插入了50w條資料後,插入速度總是會一下突然變的非常慢。這種插入變慢是斷崖式的突變,於是我冥思苦想,無意中打開了系統的資源管理器,一看發現:java佔用的記憶體在不斷飆升。 突然腦海中想到:是不是記憶體溢位了?

4.及時釋放查詢結果

        在我的資料庫查詢語句中,使用到了pres=con.prepareStatement(sql)來儲存一個sql執行狀態,使用了resultSet=pres.executeQuery來儲存查詢結果集。而在邊查邊插的過程中,我的程式碼一直沒有把查詢的結果給釋放,導致其不斷的佔用記憶體空間。當我的插入執行到50w條左右時,我的記憶體空間佔滿了,於是資料庫的插入開始不以記憶體而以磁碟為介質了,因此插入的速度就開始變得十分的低下。因此,我在每次使用完pres和resultSet後,加入了釋放其空間的語句:resultSet.close(); pres.close(); 。重新進行測試,果然,記憶體不飆升了,插入資料到50w後速度也不降低了。原來問題的本質在這裡!

        這個事情折騰了一天,也學到了很多。希望這篇部落格能幫助到大家!