A、Problem Description

1. PageHelper办法运用了静态的ThreadLocal参数,在startPage()调用紧跟MyBatis查询办法后,才会自动铲除ThreadLocal存储的目标。

2. 当一个线程先履行了A办法的PageHelper.startPage(int pageNum, int pageSize)后,在未履行到SQL句子前,因为代码抛反常而提前结束。

3. 这个线程被另一个恳求复用,根据当时的pageNum和pageSize参数,履行了B办法中的SQL句子。

4. B办法的SQL是全表扫描并查询出所有契合条件的数据,所以因为A办法的分页参数限制<<实践B办法中契合条件的数据量,导致了B办法查询结果的过错。

B、Problem inspection Steps

1. Code Review

一次因PageHelper引起的多线程复用问题的排查和处理 | 京东物流技能团队

一次因PageHelper引起的多线程复用问题的排查和处理 | 京东物流技能团队

先看一下A办法的代码就会发现,在运用了PageHelper.startPage之后,Mybatis查询SQL之前,有很多判别逻辑,而且问题就产生在中间标红的反常状况判别。

一次因PageHelper引起的多线程复用问题的排查和处理 | 京东物流技能团队

B办法在履行到第一个SQL查询句子的时候,就会因为复用线程中 PageMethod 所带有A办法中ThreadLocal的(pageNum,pageSize)参数导致B办法的查询也限制了分页参数。

2. Log Check and Prove

a. A办法提前抛反常,且没履行MyBatis查询办法的日志截图

一次因PageHelper引起的多线程复用问题的排查和处理 | 京东物流技能团队

b. B办法履行到MyBatis查询办法的截图

一次因PageHelper引起的多线程复用问题的排查和处理 | 京东物流技能团队

C、Analysis Steps

1. How to use PageHelper

a. Github Official Document Link

github.com/pagehelper/…

PageHelper 办法运用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。

只需你能够确保在 PageHelper 办法调用后紧跟 MyBatis 查询办法,这便是安全的。因为 PageHelper 在 finally 代码段中自动铲除了 ThreadLocal 存储的目标。

b. Analysis Source Code of PageHelper

i. startPage() and getLocalPage()

一次因PageHelper引起的多线程复用问题的排查和处理 | 京东物流技能团队

一次因PageHelper引起的多线程复用问题的排查和处理 | 京东物流技能团队

经过上图咱们能够发现,当一个恳求来的时候,会获取持有当时恳求的线程的ThreadLocal,调用LOCAL_PAGE.get(),查看当时线程是否有未履行的分页装备,再经过setLocalPage(page)办法设置线程的分页装备。

ii. Intercept Method in PageInterceptor

@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();
            List resultList;
            //调用办法判别是否需求进行分页,假如不需求,直接回来结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //判别是否需求进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查询总数
                    Long count = count(executor, ms, parameter, rowBounds, resultHandler, 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();
            }
        }
    }

咱们需求关注mybatis什么时候运用的这个ThreadLocal,也便是何时将分页参数获取的?

前面说到过,经过PageHelper的startPage()办法进行page缓存的设置,当程序履行sql接口mapper的办法时,就会被拦截器PageInterceptor拦截到。

PageHelper其实便是mybatis的分页插件,其完成原理便是经过拦截器的方式,pageHelper通PageInterceptor完成分页,咱们只关注intercept办法。

iii. dialect.skip(ms, parameter, rowBounds)

此处的skip办法进行设置分页参数,内部调用办法:

Page page = pageParams.getPage(parameterObject, rowBounds);

继续盯梢getPage(),发现此办法的第一行就获取了ThreadLocal的值:

Page page = PageHelper.getLocalPage();

iv. ExecutorUtil.pageQuery

resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);

这是分页办法,此办法在履行分页之前,会判别是否履行分页,根据便是前面咱们经过ThreadLocal的获取的page。

v. executor.query

resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);

这是非分页办法,咱们能够思考一下,假如ThreadLoad在运用后没有被铲除,当履行非分页的办法时,那么就会将Limit拼接到sql后面。

为什么不分也得也会拼接?咱们回头看下前面说到的dialect.skip(ms, parameterObject, rowBounds):

一次因PageHelper引起的多线程复用问题的排查和处理 | 京东物流技能团队

如上所示,只需page被获取到了,那么这个sql,就会走前面说到的ExecutorUtil.pageQuery分页逻辑,终究导致出现不行意料的状况。

其实PageHelper对于分页后的ThreaLocal是有铲除处理的。

vi. clearPage()

在intercept办法的最后,会在sql办法履行完成后,整理page缓存:

一次因PageHelper引起的多线程复用问题的排查和处理 | 京东物流技能团队

看看这个afterAll()办法:

一次因PageHelper引起的多线程复用问题的排查和处理 | 京东物流技能团队

只关注 clearPage():

一次因PageHelper引起的多线程复用问题的排查和处理 | 京东物流技能团队

vii. Conclusion

整体看下来,好像不会存在什么问题,但是咱们能够考虑集中极点状况:

•假如运用了startPage(),但是没有履行对应的sql,那么就标明,当时线程ThreadLocal被设置了分页参数,但是没有被运用,当下一个运用此线程的恳求来时,就会出现问题。

•假如程序在履行sql前,产生反常了,就没办法履行finally傍边的**clearPage()**办法,也会造成线程的ThreadLocal被污染。

所以,官方给咱们的建议,在运用PageHelper进行分页时,履行sql的代码要紧跟startPage()办法

除此之外,咱们能够手动调用clearPage()办法 ,在存在问题的办法之前。

2. How to solve the problem

1. 确保PageHelper 办法调用后紧跟 MyBatis 查询办法,在查询前不要写任何逻辑处理,因为任何代码都可能产生Exception并产生线程复用的问题。

2. 假如原有不合理的代码太多,没办法一一修改,能够考虑Controller层添加切面,JSF接口添加Filter,手动调用clearPage()办法。代码示例如下:

// 针对JSF接口的Filter
@Slf4j
public class BscJsfAspectForPageHelper extends AbstractFilter {
    public BscJsfAspectForPageHelper(){}
    @Override
    public ResponseMessage invoke(RequestMessage requestMessage) {
        try {
            log.info("BscJsfAspectForPageHelper.invoke For JSF PageHelper.clearPage()");
            PageHelper.clearPage();
        }catch (Exception e){
            log.error("BscJsfAspectForPageHelper.invoke产生反常,error msg:", e);
        }
        return getNext().invoke(requestMessage);
    }
}
// XML装备
    <bean id="bscJsfAspectForPageHelper" class="com.jdl.bsc.aspect.BscJsfAspectForPageHelper" scope="prototype">
    </bean>
// 针对Controller的切面
@Aspect
@Component
@Slf4j
public class BscAspectForPageHelper{
    @Pointcut("execution(public * com.jdl.bsc.controller.*.*(..)) ")
    public void bscAspectForPageHelper(){}
    @Before("bscAspectForPageHelper()")
    public void doBefore(JoinPoint joinPoint) {
        try {
            log.info("BscAspectForPageHelper.doBefore For PageHelper.clearPage()");
            PageHelper.clearPage();
        }catch (Exception e){
            log.error("BscAspectForPageHelper.doBefore产生反常,error msg:", e);
        }
    }
}

作者:京东物流 王崧

来源:京东云开发者社区 自猿其说 Tech 转载请注明来源