PageHelper的坑(安全使用方法)

奇怪现象

最近项目使用的PageHelper发生一个神奇的分页问题 代码没有使用分页,但是在运行的时候却发生了分页,而且还是偶现

PageHelper是怎么做到上面的问题的?

一般使用PageHelper进行分页都是在查询之前先执行:PageHelper.startPage(pageNum, pageSize);
之后紧跟查询操作;
例如:

PageHelper.startPage(pageNum, pageSize);
do()
List<A> aList= Lp.getAList(id);

正常查询后没问题,问题就在假设do()函数执行异常就会抛出异常,此时PageHelper在该线程并未清除,当其它不分页任务使用该线程时就会进行分页查询;

PageHelper分页原理

PageHelper.startPage作用:
在这里插入图片描述
ThreadLocal,独属于每个线程的本地缓存对象。
当一个请求来的时候,会获取持有当前请求的线程的ThreadLocal,调用LOCAL_PAGE.get(),查看当前线程是否有未执行的分页配置。

protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }

PageHelper会将前端传过来的参数保存到一个page对象中,然后将page的副本放到ThreadLocal中,保证多个线程之间互不影响。
如果设置了分页,即:使用了PageHelper.startPage(),那么会先运行ThreadLocal变量的set()方法,之后执行查询等逻辑,当查询逻辑正常执行完毕,最后执行ThreadLocal变量的remove()方法,将副本变量删除。
问题的关键就在于,如果在执行了ThreadLocal的set()方法成功了,但是查询等逻辑异常结束了,也就不能被执行remove()方法,那这个线程的ThreadLocal里的变量也就无法被清除,然后该线程不会被清除,而是在线程池中的状态被设置为空闲状态。
假如,有一个不需要分页的查询(会用到一个线程),去线程池中拿一个线程使用,如果恰巧用到了刚才没被清除副本变量的线程,那么就会导致查询时用到旧的变量进行分页,从而造成返回数据的错误。
解决方法参考
前面提到过,通过PageHelper的startPage()方法进行page缓存的设置,当程序执行sql接口mapper的方法时,就会被拦截器PageInterceptor拦截到。
PageHelper其实就是mybatis的分页插件,其实现原理就是通过拦截器的方式,pageHelper通PageInterceptor实现分页效果,我们只关注intercept方法:

@Override
public Object intercept(Invocation invocation) throws Throwable {
    try {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        Executor executor = (Executor) invocation.getTarget();
        CacheKey cacheKey;
        BoundSql boundSql;
        // 由于逻辑关系,只会进入一次
        if (args.length == 4) {
            //4 个参数时
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            //6 个参数时
            cacheKey = (CacheKey) args[4];
            boundSql = (BoundSql) args[5];
        }
        checkDialectExists();
        //对 boundSql 的拦截处理
        if (dialect instanceof BoundSqlInterceptor.Chain) {
            boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
        }
        List resultList;
        //调用方法判断是否需要进行分页,如果不需要,直接返回结果
        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);
        }
        return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
        if(dialect != null){
            dialect.afterAll();
        }
    }
}

只要上述do()函数正常执行不抛出异常就可以正常执行上述的dialect.afterAll();把ThreadLocal清除,要是do()有抛出异常就无法执行dialect.afterAll();
在这里插入图片描述
整体看下来,似乎不会存在什么问题,但是我们可以考虑集中极端情况:

如果使用了startPage(),但是没有执行对应的sql,那么就表明,当前线程ThreadLocal被设置了分页参数,可是没有被使用,当下一个使用此线程的请求来时,就会出现问题。

如果程序在执行sql前,发生异常了,就没办法执行finally当中的clearPage()方法,也会造成线程的ThreadLocal被污染。

所以,官方给我们的建议,在使用PageHelper进行分页时,执行sql的代码要紧跟startPage()方法。

除此之外,我们可以手动调用clearPage()方法,在存在问题的方法之前。

需要注意:不要分页的方法前手动调用clearPage,将会导致你的分页出现问题。

参考:https://segmentfault.com/a/1190000044642035

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值