Mysql大數據量查詢優化思路詳析
項目場景:
Mysql
大表查詢優化,理論上千萬級別以下的數據量Mysql單表查詢性能處理都是可以的。
問題描述:
在我們線上環境中,出現瞭mysql
幾千萬級別的日志查詢、幾百萬級別的黑名單庫查詢分頁查詢及條件查詢都慢的問題,針對Mysql表優化做瞭一些優化處理。
原因分析:
首先說一下日志查詢,在Mysql中如果索引加的比較合適,走索引情況下千萬級別查詢不會超過一秒,Mysql查詢的速度和檢索的數據條數有關。在Mybatis中,分頁查詢是先執行Count
記錄總數,再執行limit a,b 的方式來進行的,而Mysql的Count
計數方式是將所有的數據過濾一遍進行累加,因此當日志表數據過千萬時,統計一次就是十幾秒鐘的時間(這裡是服務器環境,本地情況下甚至是幾分鐘)。
limit a,b的方式也一樣,Mysql查詢時會先一條一條數到第a條,然後向後再數b條作為查詢結果,因此當起始行數越來越大時查詢同樣會變得很慢,也就是當你點第一頁時可能一下就查出來瞭,當你點最後一頁的時候可能幾十秒才能查出來。
黑名單庫查詢優化同理,也是需要通過條件優化。
在進行大批量數據落庫時,使用的Mybatis
批量插入,發現當批次數據超過3000時速度會急劇變慢,這是一個Mybatis娘胎裡自帶的問題,也需要進行解決。
解決方案:
這裡隻簡單說明優化的幾個方向。
1. 千萬級別日志查詢的優化
- 首先說下日志查詢,重點是優化無條件是分頁查詢,在無條件時,不使用
MyBatis
的分頁插件,而是自己手寫一個分頁查詢,由於MySql的count耗時過長,我們先優化他。 - 優化Count:日志表的數據隻增,不會出現中間某條刪除,所以他的數據可以理解成是連續的,我們可以在內存中直接進行計數,記錄count總數,或者給表添加一個自增的ID字段,直接
select max(id)
就是總數量,這樣count查詢的效率會提升到毫秒級別。 - 自定義分頁查詢:分頁查詢中使用優化後的count記錄總數,然後使用
(page - 1)* pageSize + 1
公式計算出當前頁的最小ID,然後將limit a,b 的Sql語句改為where ID > 最小ID limit b的方式,這樣查詢就會走索引先將小於最小ID的數據過濾掉,再進行查詢,經過第二步和第三步的優化後分頁查詢效率縮短到瞭一秒內,並且不會隨著頁數的增長而變慢。 - 條件查詢:條件查詢隻能設置合適的索引,另外慎用like '%條件%‘的方式進行匹配查詢,這樣會導致索引失效全局檢索,模糊查詢盡量使用like '條件%' 的方式進行最左匹配,也可以使用
explain+sql
語句 的方式來查看sql語句的執行效率,是否走瞭所有啥的來針對性的優化,加好合適的索引、優化查詢語句後通常一千萬以內的數據查詢效率會在3秒內。
粘出自定義分頁查詢結果封裝:
// 手動count Integer total = logPushService.queryBackCount(resMap);//查詢數量 // 手動查詢結果 List<InterceptInfo> ls = logPushService.queryBackByPage(resMap); PageInfo pageInfo = new PageInfo(); pageInfo.setTotal(total); pageInfo.setPageSize(limit); pageInfo.setList(ls); pageInfo.setPageNum(pn);
2. 幾百萬黑名單庫的查詢優化
- 黑名單庫查詢優化隻能通過加合適的索引和優化SQL語句來優化,百萬級別數據松松的在
Mysql
和Mybatis的承受范圍內,這裡是由於黑名單庫不是使用遞增的,有可能會增加也有可能會刪除,所以隻能使用優化索引和SQL的方式進行優化。 - 另外,Mybatis框架提供瞭重寫分頁查詢count統計語句的方法,隻需要將count語句命名為查詢方法_COUNT即可,例如分頁查詢的語句方法是query,那麼重寫的統計方法即為
query_COUNT
<select id="query_COUNT" parameterType="java.util.Map" resultType="java.lang.Long"> SELECT count(0) from nms_intercept_info${map.week} where 1=1 <if test="map.id!=null and map.id!='' "> AND id>#{map.id} </if> <if test="map.url!=null and map.url!='' "> AND spliturl=#{map.url} </if> <if test="map.startTime!=null and map.startTime!='' "> AND time <![CDATA[>=]]> #{map.startTime} </if> <if test="map.endTime!=null and map.endTime!='' "> AND time <![CDATA[<=]]> #{map.endTime} </if> <if test="map.type!=null and map.type!='' "> AND bigType = #{map.type} </if> </select>
3. Mybatis批量插入處理問題
Mybatis
批量插入語句中的類集合大小不能超過五千,三千是最佳,這是測試出來的結果,考慮到的原因是Mybatis會將類做反射,這個太影響效率,因此批量插入時要註意這個,如果你能夠三千三千的批量處理就限制一下,不要讓每批數據超過3000,數據量過大時也可以使用異步非阻塞的方式來插入。
異步非阻塞代碼(隻是步驟樣例,存在代碼缺失):
// 執行全量HMD導入任務的線程池 public final static ExecutorService importHasPool = Executors.newFixedThreadPool(10); public final static CompletionService<DoExcelResult> importHasPoolService = new ExecutorCompletionService<>(importHasPool); public synchronized DoExcelResult example() { // 開始執行導入 // 寫到這裡面方法最後會自動關閉 long startTime = System.currentTimeMillis(); // 定義一個集合,記錄Callable的執行結果,Callable是帶返回值的Runable List<Future<DoExcelResult>> futures = new ArrayList<>(); while ((str = reader.readLine()) != null) { if (list.size() > 5000) { // 5000插入一次 List<String> list1 = CollectionUtil.copyDepth(list); list.clear(); // BlackInfoHasImportlCallable是實現瞭Callable接口的實現類,Callable是帶返回值的Runable Future submit = SysThreadPoolCenter.importHasPoolService.submit(new BlackInfoHasImportlCallable(list1, blacklistInfoMapper)); futures.add(submit); } } // 等待執行結果 for (Future<DoExcelResult> future : futures) { try { // 2. futrue.get時會獲取返回值,線程沒執行完畢就等待等待執行結果 DoExcelResult doExcelResult = future.get(); result.setSuccessNum(result.getSuccessNum() + doExcelResult.getSuccessNum()); result.setContinueNum(result.getContinueNum() + doExcelResult.getContinueNum()); result.setErrorNum(result.getErrorNum() + doExcelResult.getErrorNum()); } catch (Exception e) { log.error(e); } } // 循環結束代表所有線程執行完畢 result.setTimeCon((System.currentTimeMillis() - startTime)/1000.0); BlacklistService.isDoing = false; } catch (Exception e) { BlacklistService.isDoing = false; log.error(e); } BlacklistService.isDoing = false; return result; }
限制每批3000條:
if (ls.size() >= 3000) { //每次保存3000 double sum = Math.ceil(ls.size() / 3000f); for (int i = 0; i < sum; i++) { total += blacklistDao.saveBatch(ls.subList(i * 3000, ((i + 1) * 3000) > ls.size() ? ls.size() : (i + 1) * 3000)); } } else { total = blacklistDao.saveBatch(ls); }
如果你數據庫用的不是mysql,而是CK或者其他的大數據處理數據庫,批量插入可能要求每秒幾萬條幾十萬條,這時就不再適合使用Myabtis
框架瞭,建議使用JDBC連接的方式,自己寫代碼拼接sql語句,再使用jdbc連接執行(使用線程池),效率上會快很多。
到此這篇關於Mysql大數據量查詢優化思路詳析的文章就介紹到這瞭,更多相關Mysql大數據量查詢優化內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- mybatis判斷int是否為空的時候,需要註意的3點
- Mybatis圖文並茂講解分頁插件
- 詳解MySQL數據庫千萬級數據查詢和存儲
- Mybatis和Mybatis-Plus時間范圍查詢方式
- 基於Java 利用Mybatis實現oracle批量插入及分頁查詢