• ThreadLocal参数设置不当导致的PageHelper分页Bug
  • 发布于 2个月前
  • 182 热度
    0 评论
最近看了个有趣的bug。大致是这么一行代码, 里面是个简单sql, 有时候返回数据, 有时候,不返回数据. 数据库没有变动, 代码也看不出问题。
xxxxxxx
mapper.findAll();
谁在搞鬼?
一通排查之后, 发现执行的SQL竟然不太对, 莫名多了几个分页参数。这参数查下来,是项目中使用的分页框架, PageHelper框架自动添加的。PageHelper 是作为 MyBatis的Interceptor工作的,执行SQL前会检查当前在ThreadLocal里设置的 分页参数, 然后利用SqlParser技术重写SQL, 一般来说不会出错。

Debug到 com.github.pagehelper.PageHelper#doBoundSql, 果然是被设置了分页。
// 堆代码 duidaima.co
public BoundSql doBoundSql(BoundSqlInterceptor.Type type, BoundSql boundSql, CacheKey cacheKey) {
    Page<Object> localPage = getLocalPage();
    BoundSqlInterceptor.Chain chain = localPage != null ? localPage.getChain() : null;
一番查找, 很快就找到了设置ThreadLocal的地方:
PageHelper.startPage(1, 10);
正常使用PageHelper的时候, 执行完SQL, interceptor会自动清理掉分页信息:
// com.github.pagehelper.PageInterceptor#intercept
finally {
    if (this.dialect != null) {
        this.dialect.afterAll();
    }
}
好巧不巧, 这里的分页后面的SQL在if条件里,竟然没有执行, 参数也就传承了下去。PageHelper和SQL要配套这个概念, 还是不够深入人心啊。

鬼从何来?
细察还有更有趣的情况:神仙代码虽设置了 ThreadLocal, 却并不在 mapper.findAll()的调用路径上。不存在先设置分页, 后调的情况,俩都不是一个调用序列,彼此都没什么关系。这种在A线程设置ThreaLocal,在B线程被看到了。这口锅,得让线程复用, 也就是 ThreadPoolExecutor来背。
用一个简单代码来复现这个场景:
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
    PageHelper.startPage(1, 10);
}).get();
for (int i = 0; i < 10; i++) {
    executor.submit(() -> {
        System.out.println(PageMethod.getLocalPage());
    });
}
这里有一个2线程的线程池执行器, 在其中一个设置上ThreadLocal, 然后提交10个任务尝试getThreadLocal。
null
null
null
Page{count=true, pageNum=1, pageSize=10, startRow=0, endRow=10, total=0, pages=0, reasonable=null, pageSizeZero=null}[]
null
Page{count=true, pageNum=1, pageSize=10, startRow=0, endRow=10, total=0, pages=0, reasonable=null, pageSizeZero=null}[]
null
Page{count=true, pageNum=1, pageSize=10, startRow=0, endRow=10, total=0, pages=0, reasonable=null, pageSizeZero=null}[]
null
Page{count=true, pageNum=1, pageSize=10, startRow=0, endRow=10, total=0, pages=0, reasonable=null, pageSizeZero=null}[]
明显能看到, 因为线程服用的存在,大约有一半时候能拿到这个ThreadLocal。任务1中设置的ThreadLocal如果不清理,会很快随着线程复用被别的任务拿到的。

用了ThreadLocal要清理虽然是常识, 但这种隐藏的用法, 还是很可能被滑过去的。自己多小心,只能多了解常用库的实现方式和注意事项,也没别的办法了。
用户评论