天天看點

關于PageHelper的坑,導緻莫名其妙的sql錯誤的問題關于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塊移除線程變量

繼續閱讀