Tech 导读
本文不仅对遇到类似问题的开发者提供了实际的解决思路,也为希望深入理解PageHelper工作机制和多线程编程的读者提供了丰富的技术细节。无论是对于中级开发者还是有经验的架构师,本文的内容都具有一定的参考价值。
导读
本文不仅对遇到类似问题的开发者提供了实际的解决思路,也为希望深入理解PageHelper工作机制和多线程编程的读者提供了丰富的技术细节。无论是对于中级开发者还是有经验的架构师,本文的内容都具有一定的参考价值。
1. PageHelper方法使用了静态的ThreadLocal参数,在startPage()调用紧跟MyBatis查询方法后,才会自动清除ThreadLocal存储的对象。
2. 当一个线程先执行了A方法的PageHelper.startPage(int pageNum, int pageSize)后,在未执行到SQL语句前,因为代码抛异常而提前结束。
3. 这个线程被另一个请求复用,根据当前的pageNum和pageSize参数,执行了B方法中的SQL语句。
Problem inspection Steps
1. Code Review
先看一下A方法的代码就会发现,在使用了PageHelper.startPage之后,Mybatis查询SQL之前,有很多判断逻辑,并且问题就发生在中间标红的异常情况判断。
2. Log Check and Prove
a. A方法提前抛异常,且没执行MyBatis查询方法的日志截图
b.B方法执行到MyBatis查询方法的截图
1. How to use PageHelper
a. Github Official Document Link
b. Analysis Source Code of PageHelper
i. startPage() and getLocalPage()
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();
List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查询总数
Long count = count(executor, ms, parameter, rowBounds, resultHandler, 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();
}
}
}
Page page = pageParams.getPage(parameterObject, rowBounds);
Page page = PageHelper.getLocalPage();
iv. ExecutorUtil.pageQuery
resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
为什么不分也得也会拼接?我们回头看下前面提到的dialect.skip(ms, parameterObject, rowBounds):
在intercept方法的最后,会在sql方法执行完成后,清理page缓存:
看看这个afterAll()方法:
只关注 clearPage():
vii. Conclusion
-
如果使用了startPage(),但是没有执行对应的sql,那么就表明,当前线程ThreadLocal被设置了分页参数,可是没有被使用,当下一个使用此线程的请求来时,就会出现问题。 -
如果程序在执行sql前,发生异常了,就没办法执行finally当中的clearPage()方法,也会造成线程的ThreadLocal被污染。
2. How to solve the problem
// 针对JSF接口的Filter
@Slf4j
public class BscJsfAspectForPageHelper extends AbstractFilter {
public BscJsfAspectForPageHelper(){}
@Override
public ResponseMessage invoke(RequestMessage requestMessage) {
try {
log.info("BscJsfAspectForPageHelper.invoke For JSF PageHelper.clearPage()");
PageHelper.clearPage();
}catch (Exception e){
log.error("BscJsfAspectForPageHelper.invoke发生异常,error msg:", e);
}
return getNext().invoke(requestMessage);
}
}
// XML配置
<bean id="bscJsfAspectForPageHelper" class="com.jdl.bsc.aspect.BscJsfAspectForPageHelper" scope="prototype">
</bean>
// 针对Controller的切面
4j
public class BscAspectForPageHelper{
"execution(public * com.jdl.bsc.controller.*.*(..)) ") (
public void bscAspectForPageHelper(){}
"bscAspectForPageHelper()") (
public void doBefore(JoinPoint joinPoint) {
try {
log.info("BscAspectForPageHelper.doBefore For PageHelper.clearPage()");
PageHelper.clearPage();
}catch (Exception e){
log.error("BscAspectForPageHelper.doBefore发生异常,error msg:", e);
}
}
}
求分享
求点赞
求在看
本篇文章来源于微信公众号:京东技术
本文来自投稿,不代表TakinTalks稳定性技术交流平台立场,如若转载,请联系原作者。
评论列表(1条)
Inspiring quest there. What occurred after? Thanks!