PageHelper 分页一直有性能问题?

压力测
• 阅读 2074

前因

项目一直使用的是PageHelper实现分页功能,项目前期数据量较少一直没有什么问题。随着业务扩增,数据库扩增PageHelper出现了明显的性能问题。

几十万甚至上百万的单表数据查询性能缓慢,需要几秒乃至十几秒的查询时间。故此特地研究了一下PageHelper源码,查找PageHelper分页的实现方式。

一段较为简单的查询,跟随debug开始源码探寻之旅。

public ResultContent select(Integer id) {
    Page<Test> blogPage = PageHelper.startPage(1,3).doSelectPage( () -> testDao.select(id));
    List<Test> test = (List<Test>)blogPage.getResult();
    return new ResultContent(0, "success", test);
}

主要保存由前端传入的pageNum(页数)、pageSize(每页显示数量)和count(是否进行count(0)查询)信息。

这里是简单的创建page并保存当前线程的变量副本心里,不做深究。

public static <E> Page<E> startPage(int pageNum, int pageSize) {
    return startPage(pageNum, pageSize, DEFAULT_COUNT);
}

public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
    return startPage(pageNum, pageSize, count, (Boolean)null, (Boolean)null);
}

public static <E> Page<E> startPage(int pageNum, int pageSize, String orderBy) {
    Page<E> page = startPage(pageNum, pageSize);
    page.setOrderBy(orderBy);
    return page;
}

public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    Page<E> page = new Page(pageNum, pageSize, count);
    page.setReasonable(reasonable);
    page.setPageSizeZero(pageSizeZero);
    Page<E> oldPage = getLocalPage();
    if(oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }

    setLocalPage(page);
    return page;
}

开始执行真正的select语句

public <E> Page<E> doSelectPage(ISelect select) {
    select.doSelect();
    return this;
}

进入MapperProxy类执行invoke方法获取到方法名称及参数值

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

接着是MapperMethod方法执行execute语句,判断是增、删、改、查。判断返回值是多个,进入executeForMany方法

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else if (SqlCommandType.FLUSH == command.getType()) {
        result = sqlSession.flushStatements();
    } else {
      throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
}

这个方法开始调用SqlSessionTemplate、DefaultSqlSession等类获取到Mapper.xml文件的SQL语句

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.<E>selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
}

开始进入PageHelper的真正实现,Plugin通过实现InvocationHandler进行动态代理获取到相关信息

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);
    }
}

PageInterceptor 实现Mybatis的Interceptor 接口,进行拦截

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) {
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            cacheKey = (CacheKey)args[4];
            boundSql = (BoundSql)args[5];
        }

        this.checkDialectExists();
        List resultList;
        if(!this.dialect.skip(ms, parameter, rowBounds)) {
            if(this.dialect.beforeCount(ms, parameter, rowBounds)) {
                Long count = this.count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                if(!this.dialect.afterCount(count.longValue(), parameter, rowBounds)) {
                    Object var12 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    return var12;
                }
            }

            resultList = ExecutorUtil.pageQuery(this.dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
        } else {
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        }

        Object var16 = this.dialect.afterPage(resultList, parameter, rowBounds);
        return var16;
    } finally {
        this.dialect.afterAll();
    }
}

转到ExecutorUtil抽象类的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 {
    if(!dialect.beforePage(ms, parameter, rowBounds)) {
        return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
    } else {
        parameter = dialect.processParameterObject(ms, parameter, boundSql, cacheKey);
        String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
        BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
        Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
        Iterator var12 = additionalParameters.keySet().iterator();

        while(var12.hasNext()) {
            String key = (String)var12.next();
            pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
        }

        return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql);
    }
}

在抽象类AbstractHelperDialect的getPageSql获取到对应的Page对象

public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
    String sql = boundSql.getSql();
    Page page = this.getLocalPage();
    String orderBy = page.getOrderBy();
    if(StringUtil.isNotEmpty(orderBy)) {
        pageKey.update(orderBy);
        sql = OrderByParser.converToOrderBySql(sql, orderBy);
    }

    return page.isOrderByOnly()?sql:this.getPageSql(sql, page, pageKey);
}

进入到MySqlDialect类的getPageSql方法进行SQL封装,根据page对象信息增加Limit。分页的信息就是这么拼装起来的

public String getPageSql(String sql, Page page, CacheKey pageKey) {
    StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
    sqlBuilder.append(sql);
    if(page.getStartRow() == 0) {
        sqlBuilder.append(" LIMIT ? ");
    } else {
        sqlBuilder.append(" LIMIT ?, ? ");
    }

    return sqlBuilder.toString();
}

将最后拼装好的SQL返回给DefaultSqlSession执行查询并返回

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}    

至此整个查询过程完成,原来PageHelper的分页功能是通过Limit拼接SQL实现的。查询效率低的问题也找出来了,那么应该如何解决。

首先分析SQL语句,limit在数据量少或者页数比较靠前的时候查询效率是比较高的。(单表数据量百万进行测试)

select * from user where age = 10 limit 1,10;结果显示0.43s

当where条件后的结果集较大并且页数达到一个量级整个SQL的查询效率就十分低下(哪怕where的条件加上了索引也不行)。

select * from user where age = 10 limit 100000,10;结果显示4.73s

那有什么解决方案呢?mysql就不能单表数据量超百万乃至千万嘛?答案是NO,显然是可以的。

SELECT a.* FROM USER a
INNER JOIN 
    (SELECT id FROM USER WHERE age = 10 LIMIT 100000,10) b 
ON a.id = b.id;

结果0.53s

完美解决了查询效率问题!!!其中需要对where条件增加索引,id因为是主键自带索引。select返回减少回表可以提升查询性能,所以采用查询主键字段后进行关联大幅度提升了查询效率。

PageHelper想要优化需要在拦截器的拼接SQL部分进行重构,由于博主能力有限暂未实现。能力较强的读者可以自己进行重构。另外,关注公众号Java技术栈,在后台回复:面试,可以获取我整理的 MySQL 系列面试题和答案,非常齐全。

附上PageHelper的git地址:

https://github.com/pagehelper...

原文链接:https://blog.csdn.net/baidu_3...

版权声明:本文为CSDN博主「岁月安然」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

近期热文推荐:

1.600+ 道 Java面试题及答案整理(2021最新版)

2.终于靠开源项目弄到 IntelliJ IDEA 激活码了,真香!

3.阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式发布,全新颠覆性版本!

5.《Java开发手册(嵩山版)》最新发布,速速下载!

觉得不错,别忘了随手点赞+转发哦!

点赞
收藏
评论区
推荐文章
菜鸟阿都 菜鸟阿都
4年前
pageHelper一对多分页解决方案
前言   pageHelper是一款优秀的Mybatis分页插件,在项目中可以非常便利的使用,使开发效率得到很大的提升,但不支持一对多结果映射的分页查询,所以在平时的使用时,对于一对多分页会出现分页错误,这篇文章主要对pageHelper分页错误进行重现以及提出解决方案。分析    mybatis进行一对多查询时,映射文件(mapper.xml
Easter79 Easter79
3年前
sql优化之大数据量分页查询(mysql)
当需要从数据库查询的表有上万条记录的时候,一次性查询所有结果会变得很慢,特别是随着数据量的增加特别明显,这时就需要使用分页查询。对于数据库分页查询,也有很多种方法和优化的点。谈优化前的准备工作为了对下面列举的一些优化进行测试,需要使用已有的一张表作为实际例子。表名:order\_history。描述:某个业务的订单历史表。主要字段
Easter79 Easter79
3年前
ssm+PageHelper实现分页查询
通过搭建ssm框架,然后通过mybatis的分页插件pagehelp进行分页查询。源码:https://gitee.com/smfx1314/pagehelper看一下项目结构:!(https://oscimg.oschina.net/oscnet/62738252a25e8c7d8d60f9f9bcf7cb51695.png)首先创建
Stella981 Stella981
3年前
SpringBoot Mybatis解决使用PageHelper一对多分页问题
SpringBootMybatis解决使用PageHelper一对多分页问题参考文章:(1)SpringBootMybatis解决使用PageHelper一对多分页问题(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.codeprj.com%2Fblog%2F
Wesley13 Wesley13
3年前
MyBatis学习总结(17)——Mybatis分页插件PageHelper
如果你也在用Mybatis,建议尝试该分页插件,这一定是最方便使用的分页插件。分页插件支持任何复杂的单表、多表分页,部分特殊情况请看重要提示(http://git.oschina.net/free/Mybatis_PageHelper/blob/master/wikis/Important.markdown)。想要使用分页插件?请看如何使用分页
Stella981 Stella981
3年前
SpringBoot使用pagehelper注意事项
Spring和SpringBoot使用pagehelper的方式不太一样,从Spring到SpringBoot的朋友注意了!如果在SpringBoot中使用Spring加载pagehelper的方式,能获取数据,但是分页不准确。SpringBoot加载pagehelper步骤如下:maven搜索springbootpagehelper!
Stella981 Stella981
3年前
Spring Boot2.X+mybatis+Druid+PageHelper实现多数据源并分页,支持多个字段动态排序,结构层级分明,代码耦合,框架入门
一、SpringBoot整合Mybatis、Druid和PageHelper并实现多数据源和分页,支持多个字段动态排序,其中对分页插件进行了封装,满足于任何场景的开发Druid是一个数据库连接池。Druid可以说是目前最好的数据库连接池!因其优秀的功能、性能和扩展性方面,深受开发人员的青睐。Druid已经在阿里巴巴部署了超过600个应用,经过一年多
Easter79 Easter79
3年前
SpringBoot使用pagehelper注意事项
Spring和SpringBoot使用pagehelper的方式不太一样,从Spring到SpringBoot的朋友注意了!如果在SpringBoot中使用Spring加载pagehelper的方式,能获取数据,但是分页不准确。SpringBoot加载pagehelper步骤如下:maven搜索springbootpagehelper!
Wesley13 Wesley13
3年前
JavaEE——Mybatis(17)
•1、PageHelper插件进行分页 JavaEE——Mybatis(18)MyBatis实用场景1.PageHelper插件分页(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.cnblogs.com%2FSkyeAngel%2Fp%2F8491741.h
Stella981 Stella981
3年前
Mybatis传多个参数(三种解决方案)
Mybatis分页插件PageHelpehttp://git.oschina.net/free/Mybatis\_PageHelper(http://git.oschina.net/free/Mybatis_PageHelper)极其方便的使用Mybatis单表的增删改查http://git.oschina.net/free/Mapper
一次因PageHelper引起的多线程复用问题的排查和解决 | 京东物流技术团队
A、ProblemDescription1\.PageHelper方法使用了静态的ThreadLocal参数,在startPage()调用紧跟MyBatis查询方法后,才会自动清除ThreadLocal存储的对象。2\.当一个线程先执行了A方法的PageH
压力测
压力测
Lv1
是谁,在寒冷的冬天,掀开被单,冰冻爱情的温暖。
文章
5
粉丝
0
获赞
0