前言
日常開發中,難免會遇到需要查詢到數據庫所有記錄的業務場景,在索引完善的情況下,當數據量達到百萬級別或者以上的時候,全表查詢就需要耗費不少的時間,這時候我們可以從以下幾個方向着手優化
優化sql
利用多線程查詢
分庫分表
今天就來討論一下使用【優化sql】和【多線程】方式提升全表查詢效率
⚠️注意,這只是簡單測試,用於講解思路,真實情況會更加的復雜,效率可能會相對受到影響,而且也會受硬件配置的影響,所以不是絕對的
前置准備
-
使用InnoDb作為執行引擎
-
創建測試表,有自增主鍵id
-
往表中添加測試數據(100W以上),可以選擇在程序中導入,也可以選擇在數據庫里面生成測試數據,具體可以參考:生成測試數據
-
Java程序中使用Mybatis來操作,使用自定義注解+SpringAOP的方式來記錄執行耗時,源碼后面會給,有興趣的朋友可以下載下來實踐一下
-
總體目錄結構
開始測試
首先確保庫中是有數據的,由於實際業務的復雜度,所以這里模擬username的時候也讓他復雜一點,不是同一條數據進行了600多萬次復制
單線程+基礎sql
再下來就是基礎的全表查詢方式,這里使用postman測試
@GetMapping("/sync")
public String getData() {
List<User> list = userService.queryAllUseSync();
return "查詢成功!";
}
@Override
@RecordMethodSpendAnnotation //這個注解標記的方法會被SpringAOP管理起來,計算方法耗時
public List<User> queryAllUseSync() {
//直接就采用Mybatis全查
return userMapper.queryAll();
}
我們來看一下,這個queryAll的sql,可以發現就是一個簡單的全表查詢
<select id="queryAll" resultMap="UserMap">
select
id, username, create_time
from performance.user
</select>
原因分析
我們直接把sql抓出來EXPLAIN一下,可以發現是沒有走索引的,全表600多W的數據,本機耗時(多次測試取平均):67s
🤔這是耗時着實是太慢了,所以必須得優化一下,那么怎么優化呢?
- 從sql出發,剛剛得sql是沒有走索引的,那么首先我們得讓sql走索引,id是個自增主鍵,我們是否可以利用主鍵進行分段查詢
單線程循環+分段sql
我們優化后的sql,其中 ?
代表的是分段的起始指 n
代表的是分段的末尾,可以看到是有走索引的
select id,username,create_time from user where id > ? and limit n
@Override
@RecordMethodSpendAnnotation
public List<User> queryAllUseSyncAndLimit(int limit) {
List<User> list = new ArrayList<>();
Long count = userMapper.getCount();
//循環次數
long cycles = count / limit;
for (int i = 0; i < cycles; i++) {
long startIdx = i * limit;
long endIdx = (i+1) * limit;
if (endIdx > count)
endIdx = count;
list.addAll(userMapper.queryAllByLimit(startIdx,Math.toIntExact(endIdx)));
}
return list;
}
🤔這個比剛剛那個還要慢太多了!!!,但是但從sql來看,確實是優化過了,那么為什么會慢這么多?而且CPU內存使用率飆升了起來
原因分析
我們使用jconsole看一下,可以發現內存占用量有點離譜,
然后再使用jstat -gcutil pid(截取一段時間的)
可以發現垃圾收集非常的頻繁,YGC達到45次,FGC達到8次,光gc耗時就達到18秒了,我的電腦是扛不住了,所以我就不繼續跑下去了
那么這種方式的問題就不在於sql了,而是程序的問題,私以為采用分頁+循環的方式,會提高效率,但是循環是需要耗費CPU資源的,由於請求的對象太大了,內存被積壓滿,所以程序就得等待有一塊合適大小的內存出現,才能進行下去
原本sql拎出來查速度是有提升的,但是現在程序必須得停下來等待內存釋放,所以CPU也會飆升,最終導致運行不下去
❌所以這種方式不可取
既然是使用分段查詢+組合的形式,那我們也可以采用多線程異步的形式,每個線程跑完數據拿出來之后就remove掉
多線程+分段sql
采用線程池的思想,核心線程設置在5個,最大線程設置在10個,關於線程數的選定網上有很多資料可以查到,這里就不贅述了
這里同時采用Future異步模式,提升效率,關於Future的認識,可以看這篇文章 Java Future模式的使用
@Override
@RecordMethodSpendAnnotation
public List<User> queryAllUseThreadPool(int limit) {
//還是獲取到總記錄數,本機是600多W測試數據
Long count = userMapper.getCount();
List<FutureTask<List<User>>> resultList = new ArrayList<>();
//分段次數
long cycles = count / limit;
for (int i = 0; i < cycles; i++) {
//每一段的起始坐標
long idx = i * limit;
log.info("idx: {}", idx);
//具體的查詢任務
FutureTask<List<User>> futureTask = new FutureTask<>(() -> userMapper.queryAllByLimit(idx,limit));
//把任務丟給線程池調度執行
threadPool.execute(futureTask);
//future異步模式,把任務放進去先,先不取結果
resultList.add(futureTask);
}
List<User> result = new ArrayList<>();
while (resultList.size() > 0) {
Iterator<FutureTask<List<User>>> iterator = resultList.iterator();
while (iterator.hasNext()) {
try {
result.addAll(iterator.next().get());
//獲取一個就刪除一個任務
iterator.remove();
} catch (InterruptedException | ExecutionException e) {
log.error("多線程查詢出現異常:{}", e.getMessage());
}
}
}
//最后一次數據可能不為整,需要額外操作
if (result.size() != count)
result.addAll(userMapper.queryAllByLimit(result.size(),Math.toIntExact(count)));
return result;
}
原因分析
對比於使用單線程+基礎sql來看,效率提升了近乎300%,限於本機性能的問題,如果采用正常的性能高點的服務器,效率更好了,我們通過jstat 看一下參數
可以發現YGC次數還是執行了29次,FGC執行了5次,對於本機來說,硬件性能有限,數據量確實是有夠大的,花在gc的時間足足有7秒鍾,所以如果還要優化的話,這里也是一個着手點
總結
導致查詢數據慢的原因有很多種,這里羅列幾種供參考
- 代碼問題(設計缺陷、sql優化沒做好等)
- 硬件資源問題(內存、I/O、CPU等等)
- 數據量太大問題
- 網絡阻塞問題
限於本人水平有限,難免會有紕漏,如果有發現文章那里寫的不對的,歡迎指出,謝謝!
最后提供一下源碼的獲取方式,關注微信公眾號【碼農Amg】,回復關鍵字:優化sql