分页插件pagehelper遇到的坑-自动添加limit

977 阅读3分钟

一、 问题描述

项目突然收到线上报警,属于sql语句执行错误,错误信息如下: image.png sql语句报错的原因是:sql语句莫名其妙被加上了一个“LIMIT ?”,并且之前也遇到过一次类似的问题,当时认为原因是sql语句人为多加了“分号”,但是错误信息中也出现了多余的“LIMIT ?”(起初认为是业务层添加了分页功能)。 原始sql是通过写死的方式实现的分页,原始sql如下: image.png

二、问题定位

  1. 报错信息提供了三处有用信息 image.png
  • 错误信息一
    在原有sql语句后面多添加了一个“LIMIT ?”
  • 错误信息二
    找到报错业务代码,了解到当前查询语句没有人为添加分页功能。

image.png

  • 错误信息三
    分页条数是50,但系统中默认分页参数是10,通过报错接口调用页面了解到两信息。 image.png
    • 消息一
      报错接口没有传分页参数,那么分页参数是10,不可能是50。
    • 消息二
      同一个页面请求了多个接口,其中有两接口有分页参数,且分页参数是50。其中有一个接口设置了分页参数,但是没有执行查询语句。

image.png

三、PageHelper原理分页原理说明

  • PageHelper插件是将分页参数封装到了ThreadLocal中,一个线程共用一个分页参数。
  • 一个线程放回线程池时,如果ThreadLocal中的值未被释放,那么Threadlocal中的数据也会伴随所属线程的生命周期存在,这样就存在ThreadLocal污染问题。
  • PageHelper插件只有执行了查询语句,才会释放ThreadLocal中的数据。 image.png

四、初步判断

本次错误的源头是程序中存在如下语句导致:

return PageHelper.startPage(query.getPage(), query.getSize());

这个语句没有执行查询数据功能,这样就不会释放ThreadLocal中的分页数据,请求完成后,线程会被放回到线程池中,当有其他请求进,且从线程池拿到的线程刚好是未释放ThreadLocal中分页数据的线程,那么这个分页数据就会被应用到当前请求中,PageHelper插件就会拦截到当前查询语句,并在语句后面添加分页关键字“LIMIT ?”

五、问题复现

  1. 测试代码
public List<Student> testOne() {
    Thread thread = Thread.currentThread();
    System.out.println("testOne: " + thread.getId() + " | " + thread.getName());
    return PageHelper.startPage(1, 2).doSelectPage(() -> studentMapper.select(new Student().setPhone("1980000000")));
}

public List<Student> testTwo() {
    Thread thread = Thread.currentThread();
    System.out.println("testOne: " + thread.getId() + " | " + thread.getName());
    List<Student> list = studentMapper.select(new Student().setPhone("1980000000"));
    return list;
}
  1. 测试结果
  • 方法testOne执行以后的线程信息

image.png

  • 方法testTwo执行以后的信息

image.png 3. 结论 使用PageHelper进行分页时,如果ThreadLocal中的分页参数没有被释放掉,那么当此线程(ThreadLocal被污染)被其他请求拿到后,就会被PageHelper分页插件的拦截器将sql进行拦截,并在sql后面拼接上分页语句。

六、问题解决

  1. 如果无需执行数据查询,只是为了返回查询分页对象,可以使用Page对象。全平台禁止使用如下语句返回空结果。
return PageHelper.startPage(query.getPage(), query.getSize());
  1. 对于使用PageHelper进行分页查询的代码中,禁止在PageHelper.startPage语句和需要执行分页的sql语句之间添加代码逻辑。因为中间代码报错,也会导致ThreadLocal被污染。
public List<Student> getList() {
    PageHelper.startPage(1, 2);
    List<Student> list = null;
    Student student = null;
    if (student  != null) {
        list = studentMapper.select(new Student().setPhone("1980000000"));
    } else {
        list = new ArrayList<>();
    }
    return list;
}
  1. 强烈建议如下写法
public List<Student> getList5() {
    return PageHelper.startPage(1, 2).doSelectPage(() -> studentMapper.select(new Student().setPhone("1980000000")));
}

或者

return new page<>();