前因
我的项目始终应用的是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 aINNER 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开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞+转发哦!