其他
一次因PageHelper引起的多线程复用问题的排查和解决
Tech导读
本文不仅对遇到类似问题的开发者提供了实际的解决思路,也为希望深入理解PageHelper工作机制和多线程编程的读者提供了丰富的技术细节。无论是对于中级开发者还是有经验的架构师,本文的内容都具有一定的参考价值。
导读
本文不仅对遇到类似问题的开发者提供了实际的解决思路,也为希望深入理解PageHelper工作机制和多线程编程的读者提供了丰富的技术细节。无论是对于中级开发者还是有经验的架构师,本文的内容都具有一定的参考价值。
01 Problem Description
在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!
1. PageHelper方法使用了静态的ThreadLocal参数,在startPage()调用紧跟MyBatis查询方法后,才会自动清除ThreadLocal存储的对象。
2. 当一个线程先执行了A方法的PageHelper.startPage(int pageNum, int pageSize)后,在未执行到SQL语句前,因为代码抛异常而提前结束。
3. 这个线程被另一个请求复用,根据当前的pageNum和pageSize参数,执行了B方法中的SQL语句。
4. B方法的SQL是全表扫描并查询出所有符合条件的数据,所以因为A方法的分页参数限定<<实际B方法中符合条件的数据量,导致了B方法查询结果的错误。02 Problem inspection Steps
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
1. Code Review
先看一下A方法的代码就会发现,在使用了PageHelper.startPage之后,Mybatis查询SQL之前,有很多判断逻辑,并且问题就发生在中间标红的异常情况判断。
2. Log Check and Prove
a. A方法提前抛异常,且没执行MyBatis查询方法的日志截图
b.B方法执行到MyBatis查询方法的截图
03 Analysis Steps
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。 1.位图原理
1. How to use PageHelper
a. Github Official Document Link
https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.mdPageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。b. Analysis Source Code of PageHelper
i. startPage() and getLocalPage()
@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();
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.pageQueryresultList = 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
1. 确保PageHelper 方法调用后紧跟 MyBatis 查询方法,在查询前不要写任何逻辑处理,因为任何代码都可能产生Exception并发生线程复用的问题。2. 如果原有不合理的代码太多,没办法一一修改,可以考虑Controller层增加切面,JSF接口增加Filter,手动调用clearPage()方法。代码示例如下:// 针对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的切面
@Aspect
@Component
@Slf4j
public class BscAspectForPageHelper{
@Pointcut("execution(public * com.jdl.bsc.controller.*.*(..)) ")
public void bscAspectForPageHelper(){}
@Before("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);
}
}
}
京东零售数据资产能力升级与实践
求分享
求点赞
求在看