SpringBoot+MyBatis-Plus進行分頁查詢與優(yōu)化
適用場景
數(shù)據(jù)量較大的單表分頁查詢
較復雜的多表關聯(lián)查詢,包含group by等無法進行count優(yōu)化較耗時的分頁查詢
技術棧
核心框架:Spring Boot + MyBatis-Plus
異步編程:JDK 8+ 的 CompletableFuture
數(shù)據(jù)庫:MySQL 8.0
線程池:自定義線程池管理并行任務(如 ThreadPoolTaskExecutor
)
實現(xiàn)思路
解決傳統(tǒng)分頁查詢中 串行執(zhí)行 COUNT 與數(shù)據(jù)查詢 的性能瓶頸,通過 并行化 減少總耗時,同時兼容復雜查詢場景(如多表關聯(lián)、DISTINCT
等)
兼容mybatisPlus分頁參數(shù),復用 IPage
接口定義分頁參數(shù)(當前頁、每頁條數(shù)),
借鑒 MyBatis-Plus 的 PaginationInnerInterceptor
,通過實現(xiàn) MyBatis 的 Interceptor
接口,攔截 Executor#query
方法,動態(tài)修改 SQL,
sql優(yōu)化適配:COUNT 優(yōu)化:自動移除 ORDER BY
,保留 GROUP BY
和 DISTINCT
(需包裹子查詢),數(shù)據(jù)查詢:保留完整 SQL 邏輯,僅追加 LIMIT
和 OFFSET。
直接上代碼
使用簡單
調(diào)用查詢方法前賦值page對象屬性total大于0數(shù)值則可進入自定義分頁查詢方案。
//示例代碼 Page<User> page = new Page<>(1,10); page.setTotal(1L);
線程池配置
@Configuration public class ThreadPoolTaskExecutorConfig { public static final Integer CORE_POOL_SIZE = 20; public static final Integer MAX_POOL_SIZE = 40; public static final Integer QUEUE_CAPACITY = 200; public static final Integer KEEP_ALIVE_SECONDS = 60; @Bean("threadPoolTaskExecutor") public ThreadPoolTaskExecutor getThreadPoolTaskExecutor() { ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); //核心線程數(shù) threadPoolTaskExecutor.setCorePoolSize(CORE_POOL_SIZE); //線程池最大線程數(shù) threadPoolTaskExecutor.setMaxPoolSize(MAX_POOL_SIZE); //隊列容量 threadPoolTaskExecutor.setQueueCapacity(QUEUE_CAPACITY); //線程空閑存活時間 threadPoolTaskExecutor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS); //線程前綴 threadPoolTaskExecutor.setThreadNamePrefix("commonTask-"); //拒絕策略 threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //線程池初始化 threadPoolTaskExecutor.initialize(); return threadPoolTaskExecutor; } @Bean("countAsyncThreadPool") public ThreadPoolTaskExecutor getCountAsyncThreadPool() { ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); //核心線程數(shù),根據(jù)負載動態(tài)調(diào)整 threadPoolTaskExecutor.setCorePoolSize(6); //線程池最大線程數(shù),根據(jù)負載動態(tài)調(diào)整 threadPoolTaskExecutor.setMaxPoolSize(12); //隊列容量 隊列容量不宜過多,根據(jù)負載動態(tài)調(diào)整 threadPoolTaskExecutor.setQueueCapacity(2); //線程空閑存活時間 threadPoolTaskExecutor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS); //線程前綴 threadPoolTaskExecutor.setThreadNamePrefix("countAsync-"); //拒絕策略 隊列滿時由調(diào)用者主線程執(zhí)行 threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //線程池初始化 threadPoolTaskExecutor.initialize(); return threadPoolTaskExecutor; } }
mybatis-plus配置類
@Configuration @MapperScan("com.xxx.mapper") public class MybatisPlusConfig { @Resource ThreadPoolTaskExecutor countAsyncThreadPool; @Resource ApplicationContext applicationContext; @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } @Bean public PageParallelQueryInterceptor pageParallelQueryInterceptor() { PageParallelQueryInterceptor pageParallelQueryInterceptor = new PageParallelQueryInterceptor(); pageParallelQueryInterceptor.setCountAsyncThreadPool(countAsyncThreadPool); pageParallelQueryInterceptor.setApplicationContext(applicationContext); return pageParallelQueryInterceptor; } }
自定義mybatis攔截器
package com.example.dlock_demo.interceptor; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.select.*; import org.apache.ibatis.builder.StaticSqlSource; import org.apache.ibatis.cache.CacheKey; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ResultMap; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.*; import org.springframework.context.ApplicationContext; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.sql.SQLException; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; /** * Mybatis-分頁并行查詢攔截器 * * @author shf */ @Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) }) @Slf4j public class PageParallelQueryInterceptor implements Interceptor { /** * 用于數(shù)據(jù)庫并行查詢線程池 */ private ThreadPoolTaskExecutor countAsyncThreadPool; /** * 容器上下文 */ private ApplicationContext applicationContext; private static final String LONG_RESULT_MAP_ID = "twoPhase-Long-ResultMap"; private static final Map<String, MappedStatement> twoPhaseMsCache = new ConcurrentHashMap(); public void setCountAsyncThreadPool(ThreadPoolTaskExecutor countAsyncThreadPool) { this.countAsyncThreadPool = countAsyncThreadPool; } public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameter = args[1]; //獲取分頁參數(shù) Page<?> page = getPageParameter(parameter); if (page == null || page.getSize() <= 0 || !page.searchCount() || page.getTotal() == 0) { return invocation.proceed(); } //獲取Mapper方法(注解形式 需利用反射且只能應用在mapper接口層,不推薦使用) /*Method method = getMapperMethod(ms); if (method == null || !method.isAnnotationPresent(PageParallelQuery.class)) { return invocation.proceed(); }*/ BoundSql boundSql = ms.getBoundSql(parameter); String originalSql = boundSql.getSql(); //禁用mybatis plus PaginationInnerInterceptor count查詢 page.setSearchCount(false); page.setTotal(0); args[2] = RowBounds.DEFAULT; CompletableFuture<Long> countFuture = resolveCountCompletableFuture(invocation, originalSql); //limit查詢 long startTime = System.currentTimeMillis(); Object proceed = invocation.proceed(); log.info("原SQL數(shù)據(jù)查詢-耗時={}", System.currentTimeMillis() - startTime); page.setTotal(countFuture.get()); return proceed; } private CompletableFuture<Long> resolveCountCompletableFuture(Invocation invocation, String originalSql) { return CompletableFuture.supplyAsync(() -> { try { //查詢總條數(shù) long startTime = System.currentTimeMillis(); long total = executeCountQuery(originalSql, invocation); log.info("分頁并行查詢COUNT總條數(shù)[{}]-耗時={}", total, System.currentTimeMillis() - startTime); return total; } catch (Throwable e) { log.error("page parallel query exception:", e); throw new CompletionException(e); } }, countAsyncThreadPool).exceptionally(throwable -> { log.error("page parallel query exception:", throwable); return 0L; }); } private CompletableFuture<Object> resolveOriginalProceedCompletableFuture(Invocation invocation) { return CompletableFuture.supplyAsync(() -> { try { long startTime = System.currentTimeMillis(); Object proceed = invocation.proceed(); log.info("原SQL數(shù)據(jù)查詢-耗時={}", System.currentTimeMillis() - startTime); return proceed; } catch (Throwable e) { throw new CompletionException(e); } }, countAsyncThreadPool).exceptionally(throwable -> { log.error("page parallel query original proceed exception:", throwable); return null; }); } /** * 執(zhí)行count查詢 */ private long executeCountQuery(String originalSql, Invocation invocation) throws JSQLParserException, SQLException { //解析并修改SQL為count查詢 Select countSelect = (Select) CCJSqlParserUtil.parse(originalSql); PlainSelect plainSelect = (PlainSelect) countSelect.getSelectBody(); //修改select為count(*) /*plainSelect.setSelectItems(Collections.singletonList( new SelectExpressionItem(new Function("COUNT", new Column("*"))) );*/ // 移除排序和分頁 Distinct distinct = plainSelect.getDistinct(); GroupByElement groupBy = plainSelect.getGroupBy(); String countSql = ""; if (groupBy == null && distinct == null) { Expression countFuncExpression = CCJSqlParserUtil.parseExpression("COUNT(*)"); plainSelect.setSelectItems(Collections.singletonList( new SelectExpressionItem(countFuncExpression))); plainSelect.setOrderByElements(null); countSql = plainSelect.toString(); } else if (groupBy != null) { plainSelect.setLimit(null); plainSelect.setOffset(null); countSql = "SELECT COUNT(*) FROM (" + plainSelect + ") TOTAL"; } else { plainSelect.setOrderByElements(null); plainSelect.setLimit(null); plainSelect.setOffset(null); countSql = "SELECT COUNT(*) FROM (" + plainSelect + ") TOTAL"; } //執(zhí)行count查詢 return doCountQuery(invocation, countSql); } /** * 執(zhí)行修改后的COUNT(*)-SQL查詢 */ @SuppressWarnings("unchecked") private Long doCountQuery(Invocation invocation, String modifiedSql) { //Executor executor = (Executor) invocation.getTarget(); //創(chuàng)建新會話(自動獲取新連接) Executor executor; SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class); try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE)) { //com.alibaba.druid.pool.DruidPooledConnection System.out.println("新會話Connection class: " + sqlSession.getConnection().getClass().getName()); Field executorField = sqlSession.getClass().getDeclaredField("executor"); executorField.setAccessible(true); executor = (Executor) executorField.get(sqlSession); Object[] args = invocation.getArgs(); MappedStatement originalMs = (MappedStatement) args[0]; Object parameter = args[1]; //創(chuàng)建新的查詢參數(shù) Map<String, Object> newParameter = new HashMap<>(); if (parameter instanceof Map) { // 復制原始參數(shù)但移除分頁參數(shù) Map<?, ?> originalParams = (Map<?, ?>) parameter; originalParams.forEach((k, v) -> { if (!(v instanceof Page)) { newParameter.put(k.toString(), v); } }); } //創(chuàng)建新的BoundSql BoundSql originalBoundSql = originalMs.getBoundSql(newParameter); BoundSql newBoundSql = new BoundSql(originalMs.getConfiguration(), modifiedSql, originalBoundSql.getParameterMappings(), newParameter); //復制原始參數(shù)值 originalBoundSql.getParameterMappings().forEach(mapping -> { String prop = mapping.getProperty(); if (mapping.getJavaType().isInstance(newParameter)) { newBoundSql.setAdditionalParameter(prop, newParameter); } else if (newParameter instanceof Map) { Object value = ((Map<?, ?>) newParameter).get(prop); newBoundSql.setAdditionalParameter(prop, value); } }); //創(chuàng)建新的BoundSql /*BoundSql originalBoundSql = originalMs.getBoundSql(parameter); BoundSql newBoundSql = new BoundSql(originalMs.getConfiguration(), modifiedSql, originalBoundSql.getParameterMappings(), parameter);*/ Configuration configuration = originalMs.getConfiguration(); //創(chuàng)建臨時ResultMap ResultMap resultMap = new ResultMap.Builder( configuration, LONG_RESULT_MAP_ID, //強制指定結果類型 Long.class, //自動映射列到簡單類型 Collections.emptyList() ).build(); if (!configuration.hasResultMap(LONG_RESULT_MAP_ID)) { configuration.addResultMap(resultMap); } String countMsId = originalMs.getId() + "_countMsId"; MappedStatement mappedStatement = twoPhaseMsCache.computeIfAbsent(countMsId, (key) -> this.getNewMappedStatement(modifiedSql, originalMs, newBoundSql, resultMap, countMsId)); //執(zhí)行查詢 List<Object> result = executor.query(mappedStatement, newParameter, RowBounds.DEFAULT, (ResultHandler<?>) args[3]); long total = 0L; if (CollectionUtils.isNotEmpty(result)) { Object o = result.get(0); if (o != null) { total = Long.parseLong(o.toString()); } } return total; } catch (Throwable e) { log.error("分頁并行查詢-executeCountQuery異常:", e); } return 0L; } private MappedStatement getNewMappedStatement(String modifiedSql, MappedStatement originalMs, BoundSql newBoundSql, ResultMap resultMap, String msId) { //創(chuàng)建新的MappedStatement MappedStatement.Builder builder = new MappedStatement.Builder( originalMs.getConfiguration(), msId, new StaticSqlSource(originalMs.getConfiguration(), modifiedSql, newBoundSql.getParameterMappings()), originalMs.getSqlCommandType() ); //復制重要屬性 builder.resource(originalMs.getResource()) .fetchSize(originalMs.getFetchSize()) .timeout(originalMs.getTimeout()) .statementType(originalMs.getStatementType()) .keyGenerator(originalMs.getKeyGenerator()) .keyProperty(originalMs.getKeyProperties() == null ? null : String.join(",", originalMs.getKeyProperties())) .resultMaps(resultMap == null ? originalMs.getResultMaps() : Collections.singletonList(resultMap)) .parameterMap(originalMs.getParameterMap()) .resultSetType(originalMs.getResultSetType()) .cache(originalMs.getCache()) .flushCacheRequired(originalMs.isFlushCacheRequired()) .useCache(originalMs.isUseCache()); return builder.build(); } /** * 獲取分頁參數(shù) */ private Page<?> getPageParameter(Object parameter) { if (parameter instanceof Map) { Map<?, ?> paramMap = (Map<?, ?>) parameter; return (Page<?>) paramMap.values().stream() .filter(p -> p instanceof Page) .findFirst() .orElse(null); } return parameter instanceof Page ? (Page<?>) parameter : null; } /** * 獲取Mapper方法 */ private Method getMapperMethod(MappedStatement ms) { try { String methodName = ms.getId().substring(ms.getId().lastIndexOf(".") + 1); Class<?> mapperClass = Class.forName(ms.getId().substring(0, ms.getId().lastIndexOf("."))); return Arrays.stream(mapperClass.getMethods()) .filter(m -> m.getName().equals(methodName)) .findFirst() .orElse(null); } catch (ClassNotFoundException e) { return null; } } }
注意事項
有人可能會擔心并行查詢,在高并發(fā)場景可能會導致count查詢與limit數(shù)據(jù)查詢不一致,但其實只要沒有鎖,只要是分開的兩條sql查詢,原mybatisplus分頁插件也一樣面臨這個問題。
count優(yōu)化沒有進行join語句判斷優(yōu)化,相當于主動關閉了page.setOptimizeJoinOfCountSql(false);在一對多等場景可能會造成count查詢有誤,Mybatisplus官網(wǎng)也有相關提示,所以這里干脆舍棄了。
mybatisplus版本不同,可能會導致JsqlParser所使用的api有所不同,需要自己對應版本修改下。本篇版本使用的3.5.1
關于線程池的線程數(shù)設置順便提一下:
網(wǎng)上流行一個說法:
1. CPU 密集型任務
特點:任務主要消耗 CPU 資源(如復雜計算、圖像處理)。
線程數(shù)建議:
- 核心線程數(shù):CPU 核心數(shù) + 1(或等于CPU核心數(shù),避免上下文切換過多)。
- 最大線程數(shù):與核心線程數(shù)相同(防止過多線程競爭 CPU)。
2. I/O 密集型任務
特點:任務涉及大量等待(如網(wǎng)絡請求、數(shù)據(jù)庫讀寫)。
線程數(shù)建議:
- 核心線程數(shù):2 * CPU 核心數(shù)(確保正常負載下的高效處理)。
- 最大線程數(shù):根據(jù)系統(tǒng)資源調(diào)整(用于應對突發(fā)高并發(fā))。
其實這個說法來源于一個經(jīng)驗公式推導而來:
threads = CPU核心數(shù) * (1 + 平均等待時間 / 平均計算時間)
《Java 虛擬機并發(fā)編程》中介紹
另一篇:《Java Concurrency in Practice》即《java并發(fā)編程實踐》,給出的線程池大小的估算公式:
Nthreads=Ncpu*Ucpu*(1+w/c),其中 Ncpu=CPU核心數(shù),Ucpu=cpu使用率,0~1;W/C=等待時間與計算時間的比率
仔細推導兩個公式,其實類似,在cpu使用率達100%時,其實結論是一致的,這時候計算線程數(shù)的公式就成了,Nthreads=Ncpu*100%*(1+w/c) =Ncpu*(1+w/c)。
那么在實踐應用中計算的公式就出來了,【以下推算,不考慮內(nèi)存消耗等方面】,如下:
1、針對IO密集型,阻塞耗時w一般都是計算耗時幾倍c,假設阻塞耗時=計算耗時的情況下,Nthreads=Ncpu*(1+1)=2Ncpu,所以這種情況下,建議考慮2倍的CPU核心數(shù)做為線程數(shù)
2、對于計算密集型,阻塞耗時趨于0,即w/c趨于0,公式Nthreads = Ncpu。
實際應用時要考慮同時設置了幾個隔離線程池,另外tomcat自帶的線程池也會共享宿主機公共資源。
以上就是SpringBoot+MyBatis-Plus進行分頁查詢與優(yōu)化的詳細內(nèi)容,更多關于SpringBoot MyBatis-Plus分頁查詢的資料請關注腳本之家其它相關文章!
相關文章
spring aop實現(xiàn)接口超時處理組件的代碼詳解
這篇文章給大家介紹了spring aop實現(xiàn)接口超時處理組件,文中有詳細的實現(xiàn)思路,并通過代碼示例給大家介紹的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下2024-02-02SpringMVC配置javaConfig及StringHttpMessageConverter示例
這篇文章主要介紹了SpringMVC配置javaConfig及StringHttpMessageConverter實現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07