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

关于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块移除线程变量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值