一次因PageHelper引起的多线程复用问题的排查和解决方案

news2024/11/17 5:50:20

目录导航

  • 导读
  • 01.Problem Description
  • 02.Problem inspection Steps
    • 1. Code Review
    • 2. Log Check and Prove
  • 03.Analysis Steps
    • 1. How to use PageHelper
      • a. Github Official Document Link
      • b. Analysis Source Code of PageHelper
    • 2. How to solve the problem

导读

本文不仅对遇到类似问题的开发者提供了实际的解决思路,也为希望深入理解PageHelper工作机制和多线程编程的读者提供了丰富的技术细节。无论是对于中级开发者还是有经验的架构师,本文的内容都具有一定的参考价值。

01.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方法查询结果的错误。

02.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查询方法的截图

图片

03.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

整体看下来,似乎不会存在什么问题,但是我们可以考虑集中极端情况:

  • 如果使用了startPage(),但是没有执行对应的sql,那么就表明,当前线程ThreadLocal被设置了分页参数,可是没有被使用,当下一个使用此线程的请求来时,就会出现问题。

  • 如果程序在执行sql前,发生异常了,就没办法执行finally当中的**clearPage()**方法,也会造成线程的ThreadLocal被污染。

所以,官方给我们的建议,在使用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);
        }
    }
}

京东技术:https://mp.weixin.qq.com/s/Zlq7zp1Gl1Gbb3ehaZgJUA

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1509767.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

作用域链的理解(超级详细)

文章目录 一、作用域全局作用域函数作用域块级作用域 二、词法作用域三、作用域链 一、作用域 作用域&#xff0c;即变量&#xff08;变量作用域又称上下文&#xff09;和函数生效&#xff08;能被访问&#xff09;的区域或集合 换句话说&#xff0c;作用域决定了代码区块中变…

狂飙Linux平台,PostgreSQL16部署大全

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

生成式 AI:使用 Pytorch 通过 GAN 生成合成数据

导 读 生成对抗网络&#xff08;GAN&#xff09;因其生成图像的能力而变得非常受欢迎&#xff0c;而语言模型&#xff08;例如 ChatGPT&#xff09;在各个领域的使用也越来越多。这些 GAN 模型可以说是人工智能/机器学习目前主流的原因&#xff1b; 因为它向每个人&#xff0…

vue+elementUI用户修改密码的前端验证

用户登录后修改密码&#xff0c;密码需要一定的验证规则。旧密码后端验证是否正确&#xff1b;前端验证新密码的规范性&#xff0c;新密码规范为&#xff1a;6-16位&#xff0c;至少含数字/字母/特殊字符中的两种&#xff1b;确认密码只需要验证与新密码是否一致&#xff1b; 弹…

数据结构——二叉树的遍历【前序、中序、后序】

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…

java-教师管理系统全部资料-164-(代码+说明)

转载地址: http://www.3q2008.com/soft/search.asp?keyword教师管理系统全部资料 第一章 综 述 2 1.1 背景说明 2 1.2 设计目的 2 1.3 系统目标 3 1.4 设计指导思想 3 1.5 开发技术概论 3 1.5.1 JSP技术 3 1.5.2 JAVA 4 1.5.3 JavaBeans 5 1.5.4 Servlet 5 1.5.5 Tomcat应用服…

纯前端Web网页内嵌AutoCAD,支持在线编辑DWG、dxf等文档。

随着企业信息化的发展&#xff0c;越来越多的企业有网页在线浏览和编辑DWG文档&#xff08;AutoCad生成的文档&#xff09;的需求&#xff0c;但是新版浏览器纷纷取消了对NPAPI插件的支持&#xff0c;导致之前一些可以在线在线浏览和编辑DWG文档纷纷失效&#xff0c;今天推荐一…

王道机试C++第 5 章 数据结构二:队列queue和21年蓝桥杯省赛选择题Day32

目录 5.2 队列 1&#xff0e;STL-queue 课上演示&#xff1a; 基本代码展示&#xff1a; 2. 队列的应用 例:约瑟夫问题 No. 2 题目描述&#xff1a; 思路提示&#xff1a; 代码展示&#xff1a; 例&#xff1a;猫狗收容所 题目描述&#xff1a; 代码表示&#xff1…

【深度学习】线性回归

Linear Regression 一个例子线性回归机器学习中的表达评价函数好坏的度量&#xff1a;损失&#xff08;Loss&#xff09;损失函数&#xff08;Loss function&#xff09;哪个数据集的均方误差 (MSE) 高 如何找出最优b和w?寻找最优b和w如何降低损失 (Reducing Loss)梯度下降法梯…

【毕设级项目】基于AI技术的多功能消防机器人(完整工程资料源码)

基于AI技术的多功能消防机器人演示效果 竞赛-基于AI技术的多功能消防机器人视频演示 前言 随着“自动化、智能化”成为数字时代发展的关键词&#xff0c;机器人逐步成为社会经济发展的重要主体之一&#xff0c;“机器换人”成为发展的全新趋势和时代潮流。在可预见的将来&#…

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《计及台区资源聚合功率的中低压配电系统低碳优化调度方法》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

软件无线电系列——软件无线电的发展历程及体系框架

本节目录 一、软件无线电的起始 二、软件无线电SDR论坛 三、SPEAKeasy计划 四、JTRS与SCA 五、软件无线电体系框架本节内容 一、软件无线电的起始 1992年5月&#xff0c;美国电信会议上&#xff0c;Joseph Mitola III博士提出来软件无线电(Software Radio,SR)的概念。理想化的…

实现支持多选的QComboBox

Qt提供的QComboBox只支持下拉列表内容的单选&#xff0c;但通过QComboBox提供的setModel、setView、setLineEdit三个方法&#xff0c;可以对QComboBox进行改造&#xff0c;使其实现下拉列表选项的多选。 QComboBox可以看作两个组件的组合&#xff1a;一个QLineEdit和一个QList…

OpenCV开发笔记(七十七):相机标定(二):通过棋盘标定计算相机内参矩阵矫正畸变摄像头图像

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/136616551 各位读者&#xff0c;知识无穷而人力有穷&#xff0c;要么改需求&#xff0c;要么找专业人士&#xff0c;要么自己研究 红胖子(红模仿)的博…

Orange3数据预处理(预处理器组件)

1.组件介绍 Orange3 提供了一系列的数据预处理工具&#xff0c;这些工具可以帮助用户在数据分析之前准备好数据。以下是您请求的预处理组件的详细解释&#xff1a; Discretize Continuous Variables&#xff08;离散化连续变量&#xff09;&#xff1a; 这个组件将连续变量转…

利用Nginx正向代理实现局域网电脑访问外网

引言 在网络环境中&#xff0c;有时候我们需要让局域网内的电脑访问外网&#xff0c;但是由于网络策略或其他原因&#xff0c;直接访问外网是不可行的。这时候&#xff0c;可以借助 Nginx 来搭建一个正向代理服务器&#xff0c;实现局域网内电脑通过 Nginx 转发访问外网的需求…

算法(结合算法图解)

算法简介简单查找二分查找法 选择排序内存的工作原理数组和链表数组选择排序小结 递归小梗 要想学会递归&#xff0c;首先要学会递归。 递归的基线条件和递归条件递归和栈小结 快速排序分而治之快速排序合并排序时间复杂度的平均情况和最糟情况小结 算法简介 算法是一组完成任…

Python3虚拟环境之virtualenv

virtualenv 在开发Python应用程序的时候&#xff0c;系统安装的Python3只有一个版本&#xff1a;3.7。所有第三方的包都会被pip安装到Python3的site-packages目录下。 如果要同时开发多个应用程序&#xff0c;这些应用程序都会共用一个Python&#xff0c;就是安装在系统的Pyt…

【算法】一维前缀和以及二维前缀和

目录 一维前缀和适用场景示例 二维前缀和适用场景一种情况另一种情况示例 一维前缀和 适用场景 求一段区间的和。 比如有一个数列 &#xff1a; 如果我们要求 [l,r]即某个区间内的数组和的时候&#xff0c;思路就是每遍历一个元素就进行求和&#xff0c;记录下加到al时的和…

HYBBS 表白墙网站PHP程序源码,支持封装成APP

PHP表白墙网站源码&#xff0c;适用于校园内或校区间使用&#xff0c;同时支持封装成APP。告别使用QQ空间的表白墙。 简单安装&#xff0c;只需PHP版本5.6以上即可。 通过上传程序进行安装&#xff0c;并设置账号密码&#xff0c;登录后台后切换模板&#xff0c;适配手机和PC…