一次因PageHelper引起的多线程复用问题的排查和解决 | 京东物流技术团队
A、Problem Description
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方法查询结果的错误。
B、Problem inspection Steps
1. Code Review
先看一下A方法的代码就会发现,在使用了PageHelper.startPage之后,Mybatis查询SQL之前,有很多判断逻辑,并且问题就发生在中间标红的异常情况判断。
B方法在执行到第一个SQL查询语句的时候,就会因为复用线程中 PageMethod 所带有A方法中ThreadLocal的(pageNum,pageSize)参数导致B方法的查询也限定了分页参数。
2. Log Check and Prove
a. A方法提前抛异常,且没执行MyBatis查询方法的日志截图
b. B方法执行到MyBatis查询方法的截图
C、Analysis Steps
1. How to use PageHelper
a. Github Official Document Link
https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md
PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。
只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。
b. Analysis Source Code of PageHelper
i. startPage() and getLocalPage()
通过上图我们可以发现,当一个请求来的时候,会获取持有当前请求的线程的ThreadLocal,调用LOCAL_PAGE.get(),查看当前线程是否有未执行的分页配置,再通过setLocalPage(page)方法设置线程的分页配置。
ii. Intercept Method in PageInterceptor
@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(); } } }
我们需要关注mybatis什么时候使用的这个ThreadLocal,也就是何时将分页参数获取的?
前面提到过,通过PageHelper的startPage()方法进行page缓存的设置,当程序执行sql接口mapper的方法时,就会被拦截器PageInterceptor拦截到。
PageHelper其实就是mybatis的分页插件,其实现原理就是通过拦截器的方式,pageHelper通PageInterceptor实现分页,我们只关注intercept方法。
iii. dialect.skip(ms, parameter, rowBounds)
此处的skip方法进行设置分页参数,内部调用方法:
Page page = pageParams.getPage(parameterObject, rowBounds);
继续跟踪getPage(),发现此方法的第一行就获取了ThreadLocal的值:
Page page = PageHelper.getLocalPage();
iv. ExecutorUtil.pageQuery
resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
这是分页方法,此方法在执行分页之前,会判断是否执行分页,依据就是前面我们通过ThreadLocal的获取的page。
v. executor.query
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
这是非分页方法,我们可以思考一下,如果ThreadLoad在使用后没有被清除,当执行非分页的方法时,那么就会将Limit拼接到sql后面。
为什么不分也得也会拼接?我们回头看下前面提到的dialect.skip(ms, parameterObject, rowBounds):
如上所示,只要page被获取到了,那么这个sql,就会走前面提到的ExecutorUtil.pageQuery分页逻辑,最终导致出现不可预料的情况。
其实PageHelper对于分页后的ThreaLocal是有清除处理的。
vi. clearPage()
在intercept方法的最后,会在sql方法执行完成后,清理page缓存:
看看这个afterAll()方法:
只关注 clearPage():
vii. Conclusion
整体看下来,似乎不会存在什么问题,但是我们可以考虑集中极端情况:
所以,官方给我们的建议,在使用PageHelper进行分页时,执行sql的代码要紧跟startPage()方法。
除此之外,我们可以手动调用clearPage()方法 ,在存在问题的方法之前。
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); } } }
作者:京东物流 王崧
来源:京东云开发者社区 自猿其说 Tech 转载请注明来源

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
基于参考物体的AIGC图像生成技术在家居导购领域的应用
本文深入探讨了基于参考物体的人工智能图像生成(AIGC)技术的最新进展。首先概述了该类技术如何发展至今,然后着重分析了两篇重要的相关学术论文。随后,文章针对家居导购领域的特殊应用场景,讨论了运用此项技术时遭遇的挑战和取得的最新效果。 引言 随着AIGC技术的快速发展,其中以Stable Diffusion模型为代表的的文生图技术已经在内容生成领域产生了应用价值,用户只需要提供一段文本输入,就能通过AI模型快速生成大量美观的图片,实现了低成本高效率的内容生成。由于文本提供的信息有限,生成的图像内容难以完全对齐用户的意图,生成的图片有时完全不是用户心中想要的内容。为了让用户用起来更加得心应手,最近一年涌现出来了大量关于控制技术的研究。比如Controlnet技术,通过将额外的多样化控制条件(如线稿图、深度图以及分割图等额外信息)应用于文生图扩散模型,可以生成对应结构和布局的图片,用户可以由此更加便捷的对生成图像进行编辑。 参考物体作为图像生成过程时的另一个控制维度,同样受到了广泛关注。基于参考物体的图像生成技术允许用户将参考图像中的物体融入到生成的图像中,同时保留参考物体的身份特征,实现对...
- 下一篇
数据质量和数据治理的关系 | 京东云技术团队
很多不太了解的人会认为:数据治理就是干数据清洗的。 近两年,在我们公司,数据治理团队在数据降本方面做的比较多,效果还不错,我们很多人可能以为:数据治理就是做数据清理的。 在京东科技集团数据治理工作组第一次全体会议上,我就讲过数据治理和数据清洗之间的关系: 数据清洗只是指通过识别和纠正数据中的错误、缺失、重复或不一致等问题,确保数据质量的过程。 我的观点是数据质量和数据治理是一体的,没有数据治理,就不可能有良好的数据质量。通过进行数据治理,我们实现了数据质量。怎么会这样?那是什么意思?让我们来了解一下。 1 数据质量管理 更准确地说,数据质量或数据质量管理侧重于确保数据符合我们的数据质量维度。数据质量有很多维度,比如: •完整性:是否填写了所有相关字段? •有效性:所有值都符合吗?地址字段是否在正确的顺序和正确的拼写? •准确性:数据是否反映了现实世界中的人或物体? 米老鼠可能不是一个真正的销售对象。 •一致性:数据是否与理解的模式一致?例如,出生日期的数据格式应为 YYYY/MM/DD/。 •唯一性:是否有重复的记录? •及时性:它是最新的吗? 数据质量确保我们的数据符合这些维度。或者...
相关文章
文章评论
共有0条评论来说两句吧...