• PageHelper分页插件隐藏的坑



    原文链接
    RocketMQ思维导图,不看会后悔哟
    Mysql思维导图分享

    上面思维导图可去gongzhonghao回复:扣扣号,获取联系方式后找我免费获得可编辑版本。 后面会继续分享其他思维导图,包括Redis、JVM、并发编程、RocketMQ、RabbtiMQ、Kafka、spring、Zookeeper、Dubbo等等

    一、问题

      2022年7月29日上午10点多的时候突然收到线上报警,问题有两个,一是大量数据库请求等待导致内存快满了,二是有sql执行报错。

      数据库等待的原因是因为运维的问题导致有些数据库不能访问,从而引发大量请求等待。所以就会有大量创建的对象不能被回收,从而导致内存满了。

      而sql报错是因为一个sql被莫名其妙的在最后面加上了limit ?,?。代码里的原sql(原sql较多,这里我只是写了个类似的sql)是 select * from station_letter order by id ;,真正执行的sql却是select * from station_letter order by id ; limit ?,?,很明显sql里的分号是不对的。

    二、分析问题

      看到追加的 limit ?,?,很容易想到是分页拦截后加上去的。在我们项目里引入了PageHelper这个组件,用到它的地方代码如下:

    PageHelper.startPage(request.getPageNum(), request.getPageSize());
    List<StationLetterListVO> list = stationLetterMapper.letterList(request);
    
    • 1
    • 2

      但是奇怪的是,在调用报错sql的方法前面没有任何地方用到PageHelper.startPage(),这就意味着报错sql不应该会被拦截处理。

      可是结果就是被拦截处理了。

      由于“是否分页处理”的值是放在ThreadLocal里的,那么我猜测应该是一个线程设置了分页值,而后又被另外的线程也拿到了该值。

      我们知道ThreadLocal里的值是线程局部变量, 它的生命周期是和线程一样的。如果是新开的线程是不会访问到别的线程里的ThreadLocal里的值的。现在的情况是一个没有分页的请求,却拿到了有分页请求的值,那只能说明肯定两个请求都是在线程池里拿到的线程,一个线程未释放threadLocal里的值就放回线程池了,然后又被别的请求拿到了。

      既然真实结果的确是拦截处理了,那么我们就只能查看PageHelper拦截的源码了,去看看是否释放了,下面是我找到的源码:

    try {
        // 处理分页,并执行查询
        Object result = this._processPage(invocation);
        var3 = result;
    } finally {
        clearLocalPage(); // 释放资源
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      从代码看到finally里有最终释放threadLocal里的分页值,照理说就不应该被其他线程拿到。这时我就猜想:一定是在调用PageHelper.startPage()之后,执行 clearLocalPage()之前,应该有地方抛异常出来,导致 clearLocalPage(); 不能执行,从而释放分页值失败。

      结合前面数据库连接不上的问题,就想到可能是因为设置分页值后,数据库的异常导致没有走释放的代码,于是走断点去查了代码执行顺序来验证。最终结果确实是猜想那样,梳理出的最终伪代码调用顺序如下:

    1. 设置分页值:PageHelper.startPage(request.getPageNum(), request.getPageSize());
    2. 获取数据库连接
    3. 处理分页:
    try {
        Object result = this._processPage(invocation);
        var3 = result;
    } finally {
        4. 释放分页值
        clearLocalPage();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

      因为数据库连不上,所以在第二步的时候就已经大量报错,导致不能执行后面的释放threadLocal值的代码。而在PageHelper里,只要threadLocal里有值,它就会将sql根据分页值进行改写,所以就会报错。

    三、解决问题

      看到问题是不是就知道怎么解决了?这其实就是一个资源释放的问题,类似释放锁资源一样,需要保证资源一定能够释放,所以业务代码在finally里加SqlUtil.clearLocalPage();就好了。SqlUtil.clearLocalPage();是我根据clearLocalPage();从PageHelper源码里找到的释放方法,所以是没有问题的。最终代码如下:

    PageHelper.startPage(request.getPageNum(), request.getPageSize());
    try {
        List<StationLetterListVO> list = stationLetterMapper.letterList(request);
        // 其它业务代码
    }finally {
        SqlUtil.clearLocalPage();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      释放资源操作,包括我们在释放锁时,不会出现漏掉释放操作的原则就是:在设置值和释放值之间不能有任何能够产生异常的代码,且释放资源要放在finally里保证一定会调用到

      后来听说PageHepler新版本已解决此问题,我们用的版本是4.1.6。官方解决办法也就是在PageHelper里加了手动释放的方法!PageHelper官方也有并发安全的分页使用方法,有兴趣的可以去看看。

      我个人是不爱用这个分页插件的。有两个原因,一是因为里面的原理不清楚,不敢乱用,代码要以安全为主;二是分页插件只能处理简单的查询,复杂的count(*)改写有问题,而且在不面向老板编程的时候,分页的地方本身就很少,而面向老板编程时,复杂的查询一堆一堆的,这个分页插件也用不了。其实自己写分页,也就多了一个count(*)查询,也不费什么事。

  • 相关阅读:
    四种自动化测试模型实例及优缺点
    c++——内联函数,auto关键字,基于范围的for循环,nullptr
    【ESD专题】案例 :走线没有强制先经过保护器件导致抗静电能力差
    遇到的问题java和mysql篇
    UML软件哪个好?10款好用的UML工具和画图软件推荐!
    网络安全工程师面试题整理
    flink生成水位线记录方式--周期性水位线生成器
    02 | docker镜像文件制作
    获取店铺所有商品API 返回值说明
    React之组件实例的三大属性之props
  • 原文地址:https://blog.csdn.net/zcl_love_wx/article/details/126069782