【實戰】利用多線程優化查詢百萬級數據


前言

日常開發中,難免會遇到需要查詢到數據庫所有記錄的業務場景,在索引完善的情況下,當數據量達到百萬級別或者以上的時候,全表查詢就需要耗費不少的時間,這時候我們可以從以下幾個方向着手優化

  1. 優化sql

  2. 利用多線程查詢

  3. 分庫分表

今天就來討論一下使用【優化sql】和【多線程】方式提升全表查詢效率

⚠️注意,這只是簡單測試,用於講解思路,真實情況會更加的復雜,效率可能會相對受到影響,而且也會受硬件配置的影響,所以不是絕對的

前置准備

  1. 使用InnoDb作為執行引擎

  2. 創建測試表,有自增主鍵id

  3. 往表中添加測試數據(100W以上),可以選擇在程序中導入,也可以選擇在數據庫里面生成測試數據,具體可以參考:生成測試數據

  4. Java程序中使用Mybatis來操作,使用自定義注解+SpringAOP的方式來記錄執行耗時,源碼后面會給,有興趣的朋友可以下載下來實踐一下

  5. 總體目錄結構

    image

開始測試

首先確保庫中是有數據的,由於實際業務的復雜度,所以這里模擬username的時候也讓他復雜一點,不是同一條數據進行了600多萬次復制

image

image

單線程+基礎sql

再下來就是基礎的全表查詢方式,這里使用postman測試

@GetMapping("/sync")
public String getData() {
    List<User> list = userService.queryAllUseSync();
    return "查詢成功!";
}


@Override
@RecordMethodSpendAnnotation //這個注解標記的方法會被SpringAOP管理起來,計算方法耗時
public List<User> queryAllUseSync() {

    //直接就采用Mybatis全查
    return userMapper.queryAll();
}

image

我們來看一下,這個queryAll的sql,可以發現就是一個簡單的全表查詢

<select id="queryAll" resultMap="UserMap">
		select
		id, username, create_time
		from performance.user
</select>
原因分析

我們直接把sql抓出來EXPLAIN一下,可以發現是沒有走索引的,全表600多W的數據,本機耗時(多次測試取平均):67s

image

🤔這是耗時着實是太慢了,所以必須得優化一下,那么怎么優化呢?

  1. 從sql出發,剛剛得sql是沒有走索引的,那么首先我們得讓sql走索引,id是個自增主鍵,我們是否可以利用主鍵進行分段查詢

單線程循環+分段sql

我們優化后的sql,其中 ?代表的是分段的起始指 n代表的是分段的末尾,可以看到是有走索引的

select id,username,create_time from user where id > ? and limit n

image

@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內存使用率飆升了起來

image

原因分析

我們使用jconsole看一下,可以發現內存占用量有點離譜

image

然后再使用jstat -gcutil pid(截取一段時間的)

image

image

可以發現垃圾收集非常的頻繁,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;
}

image

原因分析

對比於使用單線程+基礎sql來看,效率提升了近乎300%,限於本機性能的問題,如果采用正常的性能高點的服務器,效率更好了,我們通過jstat 看一下參數

可以發現YGC次數還是執行了29次,FGC執行了5次,對於本機來說,硬件性能有限,數據量確實是有夠大的,花在gc的時間足足有7秒鍾,所以如果還要優化的話,這里也是一個着手點

image

總結

導致查詢數據慢的原因有很多種,這里羅列幾種供參考

  1. 代碼問題(設計缺陷、sql優化沒做好等)
  2. 硬件資源問題(內存、I/O、CPU等等)
  3. 數據量太大問題
  4. 網絡阻塞問題

限於本人水平有限,難免會有紕漏,如果有發現文章那里寫的不對的,歡迎指出,謝謝!

最后提供一下源碼的獲取方式,關注微信公眾號【碼農Amg】,回復關鍵字:優化sql


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM