Mybatis-Pagehelper详细解析及优化插件开发

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 项目数据库数据量较大,分页查询要很久,所以要对分页优化,项目使用的分页是mybatis的Pagehelper,于是在Pagehelper的基础上进行了本次分页查询的优化

背景

项目数据库数据量较大,分页查询要很久,所以要对分页优化,项目使用的分页是mybatis的Pagehelper,于是在Pagehelper的基础上进行了本次分页查询的优化

Mybatis-Pagehelper

优化是基于mybatis-Pagehelper的,我们先看一下mybatis-Pagehelper这个插件,他是怎么实现mybatis分页的,比如,基本上我们每个人在分页查询时都会看到先查询count 再 具体查询,那他们在哪个环节查询,这里就有答案,首先我们先对这个插件进行集成

  1. 集成

    <dependency>
    
    <groupId>com.github.pagehelper</groupId>
    
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    
  2. 配置

    pagehelper.helperDialect=mysql
    
    pagehelper.reasonable=false
    
  3. 使用
    使用起来很简单,这样就可以完成了

    public PageInfo<MessageSmsSendTaskVO> queryByPage(MessageSmsSendTaskQuery messageSmsSendTaskQuery) {
    
    messageSmsSendTaskQuery.enablePage();
    
    List<MessageSmsSendTaskVO> list = this.messageSmsSendTaskMapper.queryList(messageSmsSendTaskQuery);
    
    STranslationConvertUtil.convertList(list);
    
    return toPageInfo(list);
    
    }
    
    /**
    * 初始化分页信息,且开启分页线程控制器
    */
    
    public <E> Page<E> enablePage() {
        pageInfoInit();
        return PageHelper.startPage(pageNum, pageSize);
    }
    
    /**
    * 初始化分页信息
    */
    
    public void pageInfoInit() {
        if (pageNum == null || pageNum < 1) {
        pageNum = 1;
        }
        if (pageSize == null || pageSize < 0) {
        pageSize = 15;
        }
    }
    
    4. 源码分析
    
    我们可以在idea上直接看源码,也可以在码云下载源码项目分析,

    项目地址 : http://git.oschina.net/free/Mybatis_PageHelper

    我们从PageHelper.startPage(pageNum, pageSize)这个调用开始分析
    他调用的是这个方法

    public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {

       Page<E> page = new Page<E>(pageNum, pageSize, count);
       page.setReasonable(reasonable);
       page.setPageSizeZero(pageSizeZero);
       //当已经执行过orderBy的时候
       Page<E> oldPage = getLocalPage();
       if (oldPage != null && oldPage.isOrderByOnly()) {
           page.setOrderBy(oldPage.getOrderBy());
       }
       setLocalPage(page);
       return page;

    }

    其实这个方法就一个目的就是初始化Page对象,也就是setLocalPage(page)中完成初始化

    public abstract class PageMethod {

       protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
       protected static boolean DEFAULT_COUNT = true;
    
       /**
        * 设置 Page 参数
        *
        * @param page
        */
       protected static void setLocalPage(Page page) {
           LOCAL_PAGE.set(page);
       }
    我们看到最终把实例化好的page对象放入到了ThreadLocal<Page>中,ThreadLocal之前我有写文章说过,他是线程本地变量,使用他不会存在并发问题,因为他会为每一个放在他里面的对象都开辟一块独立的内存空间。
    这个方法就结束了,我们发现后面只调用了new PageInfo<>(list)方法,并没有看到任何关于count查询的方法,那么这部分查询在哪里呢,其实是在实现了拦截器的PageInterceptor中
    

    public class PageInterceptor implements Interceptor {

       private volatile Dialect dialect;
       private String countSuffix = "_COUNT";
       protected Cache<String, MappedStatement> msCountMap = null;
       private String default_dialect_class = "com.github.pagehelper.PageHelper";
       @Override
       public Object intercept(Invocation invocation) throws Throwable {
           try {
               Object[] args = invocation.getArgs();
               MappedStatement ms = (MappedStatement) args[0];
               Object parameter = args[1];
               RowBounds rowBounds = (RowBounds) args[2];
               ResultHandler resultHandler = (ResultHandler) args[3];
               Executor executor = (Executor) invocation.getTarget();
               CacheKey cacheKey;
               BoundSql boundSql;
               //由于逻辑关系,只会进入一次
               if (args.length == 4) {
               //4 个参数时
               boundSql = ms.getBoundSql(parameter);
               cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
           } else {
               //6 个参数时
               cacheKey = (CacheKey) args[4];
               boundSql = (BoundSql) args[5];
               }
               checkDialectExists();
               //对 boundSql 的拦截处理
               if (dialect instanceof BoundSqlInterceptor.Chain) {
               boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
               }
               List resultList;
               //调用方法判断是否需要进行分页,如果不需要,直接返回结果
               if (!dialect.skip(ms, parameter, rowBounds)) {
               //判断是否需要进行 count 查询
               if (dialect.beforeCount(ms, parameter, rowBounds)) {
                   //查询总数
                   Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
                   //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                   if (!dialect.afterCount(count, parameter, rowBounds)) {
                   //当查询总数为 0 时,直接返回空的结果
                   return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                   }
               }
               resultList = ExecutorUtil.pageQuery(dialect, executor,
               ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
               } else {
               //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
               resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
               }
               return dialect.afterPage(resultList, parameter, rowBounds);
               } finally {
               if(dialect != null){
               dialect.afterAll();
           }
       }

    }

       
       这个方法里面实现了count查询和方法查询,但是其实它不仅仅是只有分页查询调用,部分页面也会调用,那他又是被谁调用的,我们继续前推
    
    
          public class Plugin implements InvocationHandler {
    
            private final Object target;
            private final Interceptor interceptor;
            private final Map<Class<?>, Set<Method>> signatureMap;
    
            private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
              this.target = target;
              this.interceptor = interceptor;
              this.signatureMap = signatureMap;
            }
    
            public static Object wrap(Object target, Interceptor interceptor) {
              Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
              Class<?> type = target.getClass();
              Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
              if (interfaces.length > 0) {
                return Proxy.newProxyInstance(
                    type.getClassLoader(),
                    interfaces,
                    new Plugin(target, interceptor, signatureMap));
              }
              return target;
            }
    
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              try {
                Set<Method> methods = signatureMap.get(method.getDeclaringClass());
                if (methods != null && methods.contains(method)) {
                  return interceptor.intercept(new Invocation(target, method, args));
                }
                return method.invoke(target, args);
              } catch (Exception e) {
                throw ExceptionUtil.unwrapThrowable(e);
              }
            }
我们看到调用他的类是Plugin类,而且是jdk动态代理来实现的,所以真相大白了,这个就是mybatis通过动态代理查询。回到PageInterceptor # intercept方法,他取出参数后通过skip方法判断要不要进行分页,我们看下skip方法的实现

```
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
    if (ms.getId().endsWith(MSUtils.COUNT)) {
        throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");
    }
    Page page = pageParams.getPage(parameterObject, rowBounds);
    if (page == null) {
        return true;
    } else {
        //设置默认的 count 列
        if (StringUtil.isEmpty(page.getCountColumn())) {
            page.setCountColumn(pageParams.getCountColumn());
        }
        autoDialect.initDelegateDialect(ms);
        return false;
    }
}
```

他会调用getPage方法获取page,老铁们这个就和开头对应上了,因为开头时我们只是做一个赋值操作,没有做其他的,而这个通过判断有没有值来判断是不是分页,所以很合理。
如果不进行分页,那么就回调用
```
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
```
这个方法就是获取mapper文件中的sql来执行查询了。这些就是分页插件的基本原理了,现在回到最开始里的问题,数据量很大,我们要提速,要基于mybatis-Pagehelper新写功能
  1. 基于mybatis-Pagehelper的插件

    新建MidPageHelper类

     public class MidPageHelper extends PageMethod implements Dialect, BoundSqlInterceptor.Chain

    这个类和PageHelper一样,也就是在调用时初始化

    PageHelperHandler类
    这个类里面就是访写PageInterceptor类

    
     @Slf4j
     @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}),
     })
     public class PageHelperHandler implements MidExpandMybatisInterceptor {
    
         private volatile Dialect dialect;
         private String countSuffix = "_MID_COUNT";
         protected Cache<String, MappedStatement> msCountMap = null;
         private String default_dialect_class = "com.dst.mid.expand.page.MidPageHelper";
    
         @Override
         public Object intercept(Invocation invocation) throws Throwable {

    我们刚刚说

    resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);

    这个方法是最后的执行方法,其实这次分页的优化也是对这里进行处理
    我们在前面加一些逻辑,增加pageQuery方法

    public static <E> List<E> pageQuery方法(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
                                        RowBounds rowBounds, ResultHandler resultHandler,
                                        BoundSql boundSql, CacheKey cacheKey) throws SQLException, JSQLParserException {
        //生成分页的缓存 key
        CacheKey pageKey = cacheKey;
        //处理参数对象
        parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
        //调用方言获取分页 sql
        //String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
        String pageSql = getPageQuerySql(boundSql.getSql(), PageHelperThreadLocal.getPage());
        BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
    
        Map<String, Object> additionalParameters = ExecutorUtil.getAdditionalParameter(boundSql);
        //设置动态参数
        for (String key : additionalParameters.keySet()) {
            pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
        }
        //对 boundSql 的拦截处理
        if (dialect instanceof BoundSqlInterceptor.Chain) {
            pageBoundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.PAGE_SQL, pageBoundSql, pageKey);
        }
        //执行分页查询
        return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
    }

    getPageSql方法就是这次优化的核心

    public static String getPageQuerySql(String sql, Page page) throws JSQLParserException {
        String innerSql = getInnerSql(sql);
        String fromSql = getFromSql(sql);
        String limit;
        if (page.getStartRow() == 0) {
            limit = "\n LIMIT ? ";
        } else {
            limit = "\n LIMIT ?, ? ";
        }
        String querySql = String.format("SELECT * FROM (%s) from_tbl INNER JOIN (%s \n %s) inner_tbl on from_tbl.id = inner_tbl.id", fromSql, innerSql, limit);
        log.info("querySql: {}", querySql);
        return querySql;
    }

    原理其实就是,先把要查看数据的id查出来,再把id结果集取一个别名和真正要查的所有数据关联获取结果,id在数据库里面是b+树中所以就算排序他也很快,如果用之前的方式,直接通过limit获取结果集再加上排序,时间就很长了,经过测试600w条数据从一分钟,优化到花费3s,缺点是对复杂查询还是有些不兼容,不过个人觉得瑕不掩瑜了。

相关文章
|
3月前
|
SQL 关系型数据库 MySQL
深入解析MySQL的EXPLAIN:指标详解与索引优化
MySQL 中的 `EXPLAIN` 语句用于分析和优化 SQL 查询,帮助你了解查询优化器的执行计划。本文详细介绍了 `EXPLAIN` 输出的各项指标,如 `id`、`select_type`、`table`、`type`、`key` 等,并提供了如何利用这些指标优化索引结构和 SQL 语句的具体方法。通过实战案例,展示了如何通过创建合适索引和调整查询语句来提升查询性能。
448 9
|
6天前
|
数据采集 机器学习/深度学习 人工智能
静态长效代理IP利用率瓶颈解析与优化路径
在信息化时代,互联网已深度融入社会各领域,HTTP动态代理IP应用广泛,但静态长效代理IP利用率未达百分百,反映出行业结构性矛盾。优质IP资源稀缺且成本高,全球IPv4地址分配殆尽,高质量IP仅占23%。同时,代理服务管理存在技术瓶颈,如IP池更新慢、质量监控缺失及多协议支持不足。智能调度系统也面临风险预判弱、负载均衡失效等问题。未来需构建分布式IP网络、引入AI智能调度并建立质量认证体系,以提升资源利用率,推动数字经济发展。
21 2
|
13天前
|
存储 人工智能 程序员
通义灵码AI程序员实战:从零构建Python记账本应用的开发全解析
本文通过开发Python记账本应用的真实案例,展示通义灵码AI程序员2.0的代码生成能力。从需求分析到功能实现、界面升级及测试覆盖,AI程序员展现了需求转化、技术选型、测试驱动和代码可维护性等核心价值。文中详细解析了如何使用Python标准库和tkinter库实现命令行及图形化界面,并生成单元测试用例,确保应用的稳定性和可维护性。尽管AI工具显著提升开发效率,但用户仍需具备编程基础以进行调试和优化。
166 9
|
22天前
|
XML SQL Java
十二、MyBatis分页插件
十二、MyBatis分页插件
53 17
|
2月前
|
前端开发 Java 数据库连接
Java后端开发-使用springboot进行Mybatis连接数据库步骤
本文介绍了使用Java和IDEA进行数据库操作的详细步骤,涵盖从数据库准备到测试类编写及运行的全过程。主要内容包括: 1. **数据库准备**:创建数据库和表。 2. **查询数据库**:验证数据库是否可用。 3. **IDEA代码配置**:构建实体类并配置数据库连接。 4. **测试类编写**:编写并运行测试类以确保一切正常。
78 2
|
2月前
|
人工智能 监控 数据可视化
提升开发效率:看板方法的全面解析
随着软件开发复杂度提升,并行开发模式下面临资源分配不均、信息传递延迟及缺乏全局视图等瓶颈问题。看板工具通过任务状态实时可视化、流量效率监控和任务依赖管理,帮助团队直观展示和解决这些瓶颈。未来,结合AI预测和自动化优化,看板工具将更高效地支持并行开发,成为驱动协作与创新的核心支柱。
|
2月前
|
JSON 供应链 搜索推荐
淘宝APP分类API接口:开发、运用与收益全解析
淘宝APP作为国内领先的购物平台,拥有丰富的商品资源和庞大的用户群体。分类API接口是实现商品分类管理、查询及个性化推荐的关键工具。通过开发和使用该接口,商家可以构建分类树、进行商品查询与搜索、提供个性化推荐,从而提高销售额、增加商品曝光、提升用户体验并降低运营成本。此外,它还能帮助拓展业务范围,满足用户的多样化需求,推动电商业务的发展和创新。
73 5
|
3月前
|
SQL Java 数据库连接
MyBatis-Plus高级用法:最优化持久层开发
MyBatis-Plus 通过简化常见的持久层开发任务,提高了开发效率和代码的可维护性。通过合理使用条件构造器、分页插件、逻辑删除和代码生成器等高级功能,可以进一步优化持久层开发,提升系统性能和稳定性。掌握这些高级用法和最佳实践,有助于开发者构建高效、稳定和可扩展的企业级应用。
170 13
|
3月前
|
机器学习/深度学习 人工智能 PyTorch
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
本文探讨了Transformer模型中变长输入序列的优化策略,旨在解决深度学习中常见的计算效率问题。文章首先介绍了批处理变长输入的技术挑战,特别是填充方法导致的资源浪费。随后,提出了多种优化技术,包括动态填充、PyTorch NestedTensors、FlashAttention2和XFormers的memory_efficient_attention。这些技术通过减少冗余计算、优化内存管理和改进计算模式,显著提升了模型的性能。实验结果显示,使用FlashAttention2和无填充策略的组合可以将步骤时间减少至323毫秒,相比未优化版本提升了约2.5倍。
103 3
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
|
3月前
|
前端开发 UED
React 文本区域组件 Textarea:深入解析与优化
本文介绍了 React 中 Textarea 组件的基础用法、常见问题及优化方法,包括状态绑定、初始值设置、样式自定义、性能优化和跨浏览器兼容性处理,并提供了代码案例。
116 8

推荐镜像

更多