由于业务需要,需要在原来的项目中增加分页查询的功能,就用到啦PageHelper,对于绝大多数的查询都能在可以接受的时间内查到但是有一个查询却慢的离谱需要四秒以上才能返回数据,打日志发现耗时也不是在执行count以及查询时发生的,而是在执行count之前有一个3到4秒的中断。
解决方案
从网上搜的解决PageHelper的方法大致有两种:
- 修改数据库表的数据库引擎为MyISAM
- 创建索引表,用来进行count查询
但这两种方法明显不适合我们,我们的数据库表中数据并不多(远未达百万),并且我们的性能问题并不是发生在sql执行的过程中,而是在执行前的三秒中断。并且企业项目岂能随便做出创建新表修改数据库引擎这种大的改动。
所以我就大胆猜测是在PageHelper组装count sql时耗费了时间(因为我们的sql特别长而且复杂,将近1000行),于是我就为超时的方法提供了自定义的count sql,这样一来PageHelper就不必再自己组装。最后的测试结果也确实证明我是对的,查询速度迅速提升到一秒以内。
原因分析
虽然问题得以解决,但组装count sql费时导致查询缓慢对我们来说仍然只是一个猜想而已,并没有什么真正的理论依据,所以我最终中决定download下PageHelper源码,从中查看找原因:
PageInterceptor.java
private Long count(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql) throws SQLException {
String countMsId = ms.getId() + countSuffix;
Long count;
//先判断是否存在手写的 count 查询
MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
if (countMs != null) {
count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
} else {
countMs = msCountMap.get(countMsId);
//自动创建
if (countMs == null) {
//根据当前的 ms 创建一个返回值为 Long 类型的 ms
countMs = MSUtils.newCountMappedStatement(ms, countMsId);
msCountMap.put(countMsId, countMs);
}
count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
}
return count;
}
count方法就是PageHelper实现count查询的表。可以看到,这个方法的指定逻辑大致如下:
- 组装count查询sql声明语句的Id。格式:mapper接口中的方法名_COUNT。例如:listUser_COUNT
- 查看是否存在自定义count语句,如果存在直接调用ExcutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler)去执行count查询,然后返回查询结果。
- 如果不存在自定义count sql,则查询“缓存(msCountMap)”
- 如果缓存不存在MappedStatement,则创建,然后存入“缓存”
- 然后调用ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler)。
大家注意,我上边对“msCountMap缓存”都用了引号。他们真的是缓存吗?或者说,他这个缓存真的对我们的组装count sql有影响吗?
显然它不是,倘若他真的缓存了PageHelper组装的count sql,那我们最多第一次查询的时候发生查询缓慢的问题,后面的查询就因该很快才对,但事实是我们每次查询都会缓慢。所以我对这个缓存里保存的东西产生了深深的怀疑。话不多说我们来看看它到底缓存了什么?
显然,它村的仅仅是我们再mapper.xml中写的sql语句而已,并不是已经组装成功的count sql(count sql 通常是:select count(0) from (我们的查询语句) tem_count)。
所以说这个缓存并不会使我们再第二次查询时免于组装count sql。
那我们继续看它在哪组装count sql,怎么组装count sql的。
CountSqlParser.java
/**
* 获取智能的countSql
*
* @param sql
* @param name 列名,默认 0
* @return
*/
public String getSmartCountSql(String sql, String name) {
//解析SQL
Statement stmt = null;
//特殊sql不需要去掉order by时,使用注释前缀
if(sql.indexOf(KEEP_ORDERBY) >= 0){
return getSimpleCountSql(sql, name);
}
try {
stmt = CCJSqlParserUtil.parse(sql);
} catch (Throwable e) {
//无法解析的用一般方法返回count语句
return getSimpleCountSql(sql, name);
}
Select select = (Select) stmt;
SelectBody selectBody = select.getSelectBody();
try {
//处理body-去order by
processSelectBody(selectBody);
} catch (Exception e) {
//当 sql 包含 group by 时,不去除 order by
return getSimpleCountSql(sql, name);
}
//处理with-去order by
processWithItemsList(select.getWithItemsList());
//处理为count查询
sqlToCount(select, name);
String result = select.toString();
return result;
}
注意stmt = CCJSqlParserUtil.parse(sql);
是的,它最终使用CCJSqlParser对整个sql语句进行了语法分析。。。这意味这我们的每一次分页查询都要进行一次极为费时的语法分析。这从安全性方面来讲无疑是合适的,但对于像我们这个将近一千行、语法结构又复杂的sql语句来说确实一场灾难。。。
总结
问题总算解决了,由于我对PageHelper这个框架也不能说是熟悉也不是啥技术大牛,不太明白作者为什么要这么做。所以不能对此做出什么评价。但是我觉得如果能够充分利用缓存的作用的话效果应该会好很多。