本次记录一次工作中偶然发现的bug,在测试环境跑流程的时候偶然发现有时候接口莫名其妙报错,刷新一下又好了。拉日志发现SQL语句上莫名其妙的拼接上了limit ? ,网上百度半天都说是开启了PageHelper,实际上报错的SQL没有启用PageHelper,研究源码才发现问题
问题日志截图:
发生问题的代码片段(公司代码要保密,怕被逮住了):
可以看出压根没使用PageHelper,但是网上搜半天找不到原因,但项目里确实是使用了PageHelper,且这个现象很像,故顺着日志往上捞,逮住这个线程看看【io-33003-exec-8】
发现在报错之前,这个线程干了外一个任务,有个日志输出,看看输出了啥
(选项目,用户角色标识,1-工程实施经理,2-施工,3-监理,4-设计。当前:0)
既然本次一进来就报错,就瞅瞅这个该死的线程上次干啥去了,是不是被玩坏了,找到上次执行的代码
这个时候发现,前人挖坑,开启了PageMethod.startPage,但是实际上没有执行任何SQL,就return跑了!!!
所以后来这个线程执行完任务就被线程池回收,又分配了新的任务。
可是为啥回收了却没回收干净能影响下个任务呢?
大家应该都能想到ThreadLoacl(ThreadLoacl内存泄漏 经典面试题属于是)
线程池中的线程是没有被销毁的,线程用完后又要回收到线程池中的。如果一个线程不销毁,那么跟随这个线程的ThreadLocalMap就一直存在,上次变量的变更,下次依然在上面使用。
可是本次问题好像和ThreadLocal无关欸?
让我们瞅瞅PageHelper
贴源码:
PageMethod.startPage方法:
注意到startPage方法最后是调用了setLocalPage
setLocalPage方法:
可以看出最后是对ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();进行了设置
所以导致该线程本次执行的任务却拿到了上次任务的参数
可是又有问题了,那正常使用PageHelper为啥不会出现这些问题呢?为什么刷新一下又不报错了呢?
顺着源码看下去,会发现在PageHelper后续执行,在mybaits拼接参数执行完SQL后(无论本次SQL会不会正常运行还是报错),会执行clearPage这个方法(因为afterAll这个方法在trycatch执行SQL的finally中了)
clearPage:
所以在PageMethod.startPage后,下一次执行的SQL会被影响到,即使本次请求已经结束,但是存在ThreadLoacl中的参数,还是会影响线程复用执行的下一次请求。
解决方法也有三种:
(1)PageMethod.startPage后面紧紧跟随sql语句,保证每个startPage方法都执行了sql语句,执行完sql语句后PageHelper会调用clearPage方法清除theardLocal里的参数(此处只能保证你写的,你永远也不知道其他人给你埋了什么坑,本次发现问题实际上已经上线生产大半年了)
(2)每次执行方法或后,都调用clearPage方法,但是过于麻烦
(3)在拦截器中调用PageMethod.clearPage(推荐)(若有AOP实现日志处理,也可塞到AOP中处理)