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);

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

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

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

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

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

try {
    // 处理分页,并执行查询
    Object result = this._processPage(invocation);
    var3 = result;
} finally {
    clearLocalPage(); // 释放资源
}

  从代码看到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();
}

  因为数据库连不上,所以在第二步的时候就已经大量报错,导致不能执行后面的释放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();
}

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

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

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

你可能感兴趣的:(java,mybatis,PageHelper,ThreadLocal)