Mybatis-PageHelper
Mybatis-PageHelper copied to clipboard
未进行PageHelper.startPage指定翻页时,查询携带LIMIT参数,ThreadLocal清理失败
- [x] 我已在 issues 搜索类似问题,并且不存在相同的问题.
异常模板
无明显异常,已尽力排查,并提供目前排查结论,但还是对根本问题不够清楚,还望赐教
使用环境
- PageHelper 版本: 5.2.0
- 数据库类型和版本: mysql5.7
- JDBC_URL: jdbc:mysql://x:x/x?characterEncoding=UTF-8&useAffectedRows=true&allowMultiQueries=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&useSSL=false${database.url-param}
SQL 解析错误
无
分页参数
无
String themeKey = dto.getThemeKey();
List<String> productNoList = dto.getProductNoList();
if(CollectionUtils.isEmpty(productNoList)){
return Collections.emptyList();
}
Example example = new Example(AscriptionProductDO.class);
Example.Criteria criteria = example.createCriteria();
//todo 单传themekey有问题
if(StringUtils.isNotEmpty(themeKey)){
criteria.andEqualTo("themeKey", themeKey);
}
if(!CollectionUtils.isEmpty(productNoList)){
criteria.andIn("productNo", productNoList);
}
criteria.andEqualTo("isDelete", 0);
List<AscriptionProductDO> ascriptionProductList = ascriptionProductDOMapper.selectByExample(example);
if(CollectionUtils.isEmpty(ascriptionProductList)){
return Collections.emptyList();
}
return JSON.parseArray(JSON.toJSONString(ascriptionProductList), AscriptionProductDTO.class);
原 SQL
由example组装sql
DEBUG 2022-07-19 19:09:59,784 [] [http-nio-8033-exec-9] c.t.o.m.A.selectByExample:137 - ==> Preparing: SELECT id,batch_no,store_no,platform,theme_key,ascription_scene,ascription_no,product_code,product_no,product_name,version,arrival_time,is_invoke_chain,gmt_invoke,audit_status,level,level_name,channel,terminal,contract_name,author,allocate_status,is_delete,ext_info,gmt_create,gmt_modify,privilege_status,extra_data,video_file,model_file FROM `ascription_product` WHERE ( ( product_no in ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? ) and is_delete = ? ) )
期望的结果:
DEBUG 2022-07-19 19:09:59,784 [] [http-nio-8033-exec-9] c.t.o.m.A.selectByExample:137 - ==> Preparing: SELECT id,batch_no,store_no,platform,theme_key,ascription_scene,ascription_no,product_code,product_no,product_name,version,arrival_time,is_invoke_chain,gmt_invoke,audit_status,level,level_name,channel,terminal,contract_name,author,allocate_status,is_delete,ext_info,gmt_create,gmt_modify,privilege_status,extra_data,video_file,model_file FROM `ascription_product` WHERE ( ( product_no in ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? ) and is_delete = ? ) )
DEBUG 2022-07-19 19:07:40,110 [] [http-nio-8033-exec-9] c.t.o.m.A.selectByExample:137 - ==> Preparing: SELECT id,batch_no,store_no,platform,theme_key,ascription_scene,ascription_no,product_code,product_no,product_name,version,arrival_time,is_invoke_chain,gmt_invoke,audit_status,level,level_name,channel,terminal,contract_name,author,allocate_status,is_delete,ext_info,gmt_create,gmt_modify,privilege_status,extra_data,video_file,model_file FROM `ascription_product` WHERE ( ( product_no in ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? ) and is_delete = ? ) ) LIMIT ?
完整异常信息
无异常打印,主要表现为:不是每次都有limit,但重试多次就会出现
初步排查过程及结论
1、java代码并不存在翻页代码,但sql却存在limit,推断线程变量串用
重现场景:
1、抓取打印sql时业务线程编号
从上图查看源码得知,总共打印2条sql的源码出处为 com.github.pagehelper.Dialect#beforeCount com.github.pagehelper.util.ExecutorUtil#pageQuery
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查询总数
Long count = count(executor, ms, parameter, rowBounds, null, 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);
}
正常不添加limit情况走else分支,论证如图
2、跟进dialect.skip 发现时LocalPage会获取到Page对象
com.github.pagehelper.page.PageParams#getPage
com.github.pagehelper.page.PageMethod#getLocalPage
方法执行轨迹显示(只显示进入方法行,足以证明page不为空)
Page page = PageHelper.getLocalPage();
if (page == null) {
if (rowBounds != RowBounds.DEFAULT) {
if (offsetAsPageNum) {
page = new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount);
} else {
page = new Page(new int[]{rowBounds.getOffset(), rowBounds.getLimit()}, rowBoundsWithCount);
//offsetAsPageNum=false的时候,由于PageNum问题,不能使用reasonable,这里会强制为false
page.setReasonable(false);
}
if(rowBounds instanceof PageRowBounds){
PageRowBounds pageRowBounds = (PageRowBounds)rowBounds;
page.setCount(pageRowBounds.getCount() == null || pageRowBounds.getCount());
}
} else if(parameterObject instanceof IPage || supportMethodsArguments){
try {
page = PageObjectUtil.getPageFromObject(parameterObject, false);
} catch (Exception e) {
return null;
}
}
if(page == null){
return null;
}
PageHelper.setLocalPage(page);
}
//分页合理化
if (page.getReasonable() == null) {
page.setReasonable(reasonable);
}
//当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
if (page.getPageSizeZero() == null) {
page.setPageSizeZero(pageSizeZero);
}
return page;
3、实际是否有执行线程变量清除操作 com.github.pagehelper.PageInterceptor#intercept
finally {
if(dialect != null){
dialect.afterAll();
}
}
向上推断调用栈正常
所以 protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>(); 的问题实在是。。不知道在哪~
其他类型的错误
功能建议
详细说明,尽可能提供(伪)代码示例。
看看这里:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md#3-pagehelper-%E5%AE%89%E5%85%A8%E8%B0%83%E7%94%A8
有可能其他地方使用不规范导致没有消费和清理。
感谢您的回复!
- 什么时候会导致不安全的分页? PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。
只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。
如果代码在进入 Executor 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement 时), 这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。
但是如果你写出下面这样的代码,就是不安全的用法:
PageHelper.startPage(1, 10);
List<User> list;
if(param1 != null){
list = userMapper.selectIf(param1);
} else {
list = new ArrayList<User>();
}
这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
上面这个代码,应该写成下面这个样子:
List<User> list;
if(param1 != null){
PageHelper.startPage(1, 10);
list = userMapper.selectIf(param1);
} else {
list = new ArrayList<User>();
}
这种写法就能保证安全。
如果你对此不放心,你可以手动清理 ThreadLocal 存储的分页参数,可以像下面这样使用:
List<User> list;
if(param1 != null){
PageHelper.startPage(1, 10);
try{
list = userMapper.selectAll();
} finally {
PageHelper.clearPage();
}
} else {
list = new ArrayList<User>();
}
这么写很不好看,而且没有必要。
1、之前考虑过线程复用threadLocal问题,但连续请求每次都打印的不同线程号, 2、是否有使用pageHelper.start但是未消费情况?目前看了没有,只有一个start,但是mapper在service中,代码行距离较远但这不是问题把,A线程的threadlocal遇到sql在PageIntercepter拦截始终会被清理的 3、目前根据文章排查使用的springboot-pageHelper-start,配置保持是一致,还有其他思路提供嘛?
容易复现的情况下可以加断点看看何时创建和消费。
容易复现的情况下可以加断点看看何时创建和消费。
目前最大的问题是本地很难复现~只能在测试环境;我试试直接断点远程把
新版本增加了debug=true的调试模式,执行真正查询时,会输出设置分页参数时的堆栈信息,通过比对可以找到错误调用的地方。