一、 问题描述
项目突然收到线上报警,属于sql语句执行错误,错误信息如下:
sql语句报错的原因是:sql语句莫名其妙被加上了一个“LIMIT ?”,并且之前也遇到过一次类似的问题,当时认为原因是sql语句人为多加了“分号”,但是错误信息中也出现了多余的“LIMIT ?”(起初认为是业务层添加了分页功能)。
原始sql是通过写死的方式实现的分页,原始sql如下:
二、问题定位
- 报错信息提供了三处有用信息
- 错误信息一
在原有sql语句后面多添加了一个“LIMIT ?” - 错误信息二
找到报错业务代码,了解到当前查询语句没有人为添加分页功能。
- 错误信息三
分页条数是50,但系统中默认分页参数是10,通过报错接口调用页面了解到两信息。- 消息一
报错接口没有传分页参数,那么分页参数是10,不可能是50。 - 消息二
同一个页面请求了多个接口,其中有两接口有分页参数,且分页参数是50。其中有一个接口设置了分页参数,但是没有执行查询语句。
- 消息一
三、PageHelper原理分页原理说明
- PageHelper插件是将分页参数封装到了ThreadLocal中,一个线程共用一个分页参数。
- 一个线程放回线程池时,如果ThreadLocal中的值未被释放,那么Threadlocal中的数据也会伴随所属线程的生命周期存在,这样就存在ThreadLocal污染问题。
- PageHelper插件只有执行了查询语句,才会释放ThreadLocal中的数据。
四、初步判断
本次错误的源头是程序中存在如下语句导致:
return PageHelper.startPage(query.getPage(), query.getSize());
这个语句没有执行查询数据功能,这样就不会释放ThreadLocal中的分页数据,请求完成后,线程会被放回到线程池中,当有其他请求进,且从线程池拿到的线程刚好是未释放ThreadLocal中分页数据的线程,那么这个分页数据就会被应用到当前请求中,PageHelper插件就会拦截到当前查询语句,并在语句后面添加分页关键字“LIMIT ?”
五、问题复现
- 测试代码
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;
}
- 测试结果
- 方法testOne执行以后的线程信息
- 方法testTwo执行以后的信息
3. 结论
使用PageHelper进行分页时,如果ThreadLocal中的分页参数没有被释放掉,那么当此线程(ThreadLocal被污染)被其他请求拿到后,就会被PageHelper分页插件的拦截器将sql进行拦截,并在sql后面拼接上分页语句。
六、问题解决
- 如果无需执行数据查询,只是为了返回查询分页对象,可以使用Page对象。全平台禁止使用如下语句返回空结果。
return PageHelper.startPage(query.getPage(), query.getSize());
- 对于使用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;
}
- 强烈建议如下写法
public List<Student> getList5() {
return PageHelper.startPage(1, 2).doSelectPage(() -> studentMapper.select(new Student().setPhone("1980000000")));
}
或者
return new page<>();