关于PageHelper的坑,导致莫名其妙的sql错误的问题

有目标就不怕路远。年轻人.无论你现在身在何方.重要的是你将要向何处去。只有明确的目标才能助你成功。没有目标的航船.任何方向的风对他来说都是逆风。因此,再遥远的旅程,只要有目标.就不怕路远。没有目标,哪来的劲头?一车尔尼雷夫斯基

导读:本篇文章讲解 关于PageHelper的坑,导致莫名其妙的sql错误的问题,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

关于PageHelper的坑,导致莫名其妙的sql错误的问题

前言

PageHelper 用得好,能省很多功夫。用得不好会埋下很大的隐患,并且很难发现。本文就是讨论这个很难发现的坑。

症状

  • 出现莫名其妙的分页混乱

原来不分页的sql却只查出了部分数据,观察sql发现sql被添加了limit

  • 或者sql语句报错

观察该sql语句有limit,但limit后面又被添加了limit

  • 上述症状比较难重现。但如果知道其发病机理,则100%可重现。

对阵下药

这是因为PageHelper实现的原理是通过ThreadLocal实现。在PageHelper.startPage()的时候,将分页的信息绑定到线程A,当执行mapper的方法的时候,sql被PageHelper的拦截器拦截并取出放入线程A的分页信息。

接着sql被分解为查询记录总数的sql,如果总数不为0,再执行查询分页数据的sql(原sql被改造成末尾带有limit),最后在拦截器的finally中移除绑定到线程A中的分页信息。

上述是正常情况。

假设分页数据绑定到线程A最终没被移除,那别的方法,甚至是不相关的方法在被请求时,若分配线程A为其执行任务,则可以获取到分页信息,导致PageHelper框架以为需要分页(tomcat连接池的线程对象可能出现复用)

项目使用PageHelper,但是有些同事使用不规范,如下

// 往线程中绑定分页信息,却没有移除,造成复用该线程的方法出现分页的现象
PageHelper.startPage(pageNum, pageSize);
return new ResponseDTO<>(new PageInfo<>(userQueryService.queryPage(name)));

如何避免

1、规范编码

PageHelper.startPage()之后请紧跟mapper的查询方法。中间不要隔任何代码,中间存在任何代码就存在发生异常导致mapper方法未被执行的可能性。

检查mapper.xxx(p.getId) 是否会发生NPE,如p是null或p.getId是null且接收的数据类型是primitive type。

2、全局上设置拦截器移除线程变量

在拦截器里调用移除变量的方法最好是用顺序最靠前的javax.servlet.Filter,调用 PageHelper.startPage() 移除ThreadLocal中的变量

补充其他风险

可能导致本文的”坑”的情况

情况1:
PageHelper.startPage(pageNum, pageSize)
// 后面没有xxxMapper.xxxx()

情况2:
PageHelper.startPage(pageNum, pageSize)
int i = 1/0;// 但是在这里发生了空指针异常
xxxMapper.xxxx()

情况3:
PageHelper.startPage(pageNum, pageSize)
// xxxMapper是null,或p是null,或p.getId是null且接收的数据类型是primitive type
xxxMapper.xxx(p.getId)


情况4:
PageHelper.startPage(pageNum, pageSize)
// 调用了其他service,而这个service还未执行到它的mapper方法的时候发生了异常
xxxService.xxxx()

动手做实验

下面展示了我是如何100%找出这个问题的

1、准备

  • 配置tomcat连接池数量为1,永远复用这个,方便重现问题
server.tomcat.max-threads=1
  • 写一个方法现查是否真的永远复用这个线程
@RequestMapping(value = "/test/currentThread", method = RequestMethod.GET)
public Object currentThread() {
    return Thread.currentThread().getId();
}

在浏览器中反复请求该方法,确认id只有一个。

要注意不要用chrome,chrome有病,当server.tomcat.max-threads=2的时候经常id不变,本人使用搜狗浏览器。PS: 搜狗浏览器在测负载均衡的时候也是相当好用主要没缓存,每次请求都能看到轮询)

PS:这是比较蠢的方法确定线程服用数是1,请大神们用别的方法。另外从逻辑学上,这个只能证明 “还没出现”,但 “还没出现” 不代表不出现,所以用于证明 “线程池只有这个id的线程” 是有点瑕疵的。

2、设置ThreadLocal变量但不移除

// 该Controller是 @RestController
@RequestMapping(value = "/test/testPageHelper", method = RequestMethod.GET)
public Obect testPageHelper() {
    theadIdOfSetThreadLocal = Thread.currentThread().getId();
    System.out.println("当前线程id是:" + theadIdOfSetThreadLocal);
    PageHelper.startPage(1, 10);
    System.out.println("获取绑定在线程的变量===>" + PageHelper.getLocalPage());
    return null;
}

3、执行出错

@RequestMapping(value = "/test/testPageHelper2", method = RequestMethod.GET)
public ResponseDTO<Object> testPageHelper2() {
    System.out.println("是否有===>" + PageHelper.getLocalPage());
    long curThreadId = Thread.currentThread().getId();
    System.out.println("当前线程id===>" + curThreadId);
    if (curThreadId == theadIdOfSetThreadLocal) {
        System.out.println("tomcat的线程池复用了");
    }
    User cond = new User();
    // 情况一:这条语句不带limit,不会出错,但是会查出分页数据,即查不出全部数据
    // Object obj = userMapper.select(cond);
    
    // 情况二:这条sql语句本身带了limit,分页追加limit后会有2个limit,会出错
    Object obj = userMapper.testLimit(1);
    return new ResponseDTO<>(obj);
}

可以看到,查询的sql语句被改造了。如果此时sql语句是带limit的,就会出现sql语法错误!当再调用一次 /test/testPageHelper2 就能正常查询了,这是因为 Object obj = userMapper.testLimit();执行不管成功还是失败,都会触发PageHelper.clearPage(),第二次查的时候线程绑定的分页信息就被移除了!

4、补充一些出错场景

例 1
@RequestMapping(value = "/test/testPageHelper2", method = RequestMethod.GET)
public ResponseDTO<Object> testPageHelper2() {
    System.out.println("是否有===>" + PageHelper.getLocalPage());
    long curThreadId = Thread.currentThread().getId();
    System.out.println("当前线程id===>" + curThreadId);
    if (curThreadId == theadIdOfSetThreadLocal) {
        System.out.println("tomcat的线程池复用了");
    }
    User cond = new User();
    // 异常导致未进入mapper方法,ThreadLocal的变量未能清除
    int i = 8/0;
    //userMapper = null;
    Object obj = userMapper.select(cond);
    return new ResponseDTO<>(obj);
}

无论是int i = 8/0; 还是 userMapper = null;,将导致未进入mapper方法,ThreadLocal的变量未能清除

例 2
@RequestMapping(value = "/test/testPageHelper2", method = RequestMethod.GET)
public ResponseDTO<Object> testPageHelper2() {
    System.out.println("是否有===>" + PageHelper.getLocalPage());
    long curThreadId = Thread.currentThread().getId();
    System.out.println("当前线程id===>" + curThreadId);
    if (curThreadId == theadIdOfSetThreadLocal) {
        System.out.println("tomcat的线程池复用了");
    }
    
    // 由于接受参数是primitive类型,null转不了int报错,并未进入mybatis拦截器,所以并不会移除线程的分页数据,当再次进入该方法依然可以获取到绑定在线程的数据
    Integer limit = null;
    Object obj = userMapper.testLimit(limit);
    
    
    return new ResponseDTO<>(obj);
}


// userMapper.testLimit 的声明
@Select("SELECT * FROM user LIMIT #{limit}")
User testLimit(int limit);

由于接受参数是primitive类型,null转不了int报错,并未进入mybatis拦截器,所以并不会移除线程的分页数据,当再次进入该方法依然可以获取到绑定在线程的数据

例 3
@RequestMapping(value = "/test/testPageHelper2", method = RequestMethod.GET)
public ResponseDTO<Object> testPageHelper2() {
    System.out.println("是否有===>" + PageHelper.getLocalPage());
    long curThreadId = Thread.currentThread().getId();
    System.out.println("当前线程id===>" + curThreadId);
    if (curThreadId == theadIdOfSetThreadLocal) {
        System.out.println("tomcat的线程池复用了");
    }
    

    
    // 下面这种情况虽然传了null,但 `userMapper.select(cond)` 接收的时候并不会报错,只是接收后从cond中get字段的时候报错,这时候已经进入了mybatis拦截器,即使异常,也会进入finally块移除线程变量
    User cond = null;
    Object obj = userMapper.select(cond);
    
    
    return new ResponseDTO<>(obj);
}


// userMapper.select(cond) 的声明
@SelectProvider(type = SqlTemplate.class,method = "select")
List<T> select(T record);

这种情况虽然传了null,但 userMapper.select(cond) 接收的时候并不会报错,只是接收后从cond中get字段的时候报错,这时候已经进入了mybatis拦截器,即使异常,也会进入finally块移除线程变量

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/135319.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!