1.PageHelper 方法使用靜態 ThreadLocal 引數,該引數在 startpage() 呼叫後自動清除 ThreadLocal 物件,緊跟 Mybatis 查詢方法。
2.當執行緒首次執行方法 a 的 pagehelper 時startpage(int pagenum, int pagesize),在執行 SQL 語句之前,它過早結束,因為 ** 丟擲異常。
3.此執行緒由另乙個請求重用,並基於當前的 pagenum 和 pagesize 引數,執行方法 b 中的 SQL 語句。
4.方法B的SQL掃瞄整個表,查詢出所有滿足條件的資料,因此由於方法A的分頁引數限制了實際方法B中滿足條件的資料量,因此方法B的查詢結果不正確。
如果你看一下方法a的**,你會發現你正在使用pagehelper在startpage之後,Mybatis查詢SQL之前有很多判斷邏輯,問題發生在紅色異常判斷的中間。
當方法 B 執行第乙個 SQL 查詢語句時,方法 B 的查詢也會因為重用執行緒中方法 A 中的 threadlocal(pagenum, pagesize)引數而限制分頁引數。
a.方法 A 提前丟擲異常且不執行 mybatis 查詢方法的日誌截圖。
b.對 mybatis 查詢方法執行的方法 b 的截圖。
pagehelper 方法使用靜態 threadlocal 引數,並且 pagination 引數繫結到執行緒。
只要你能保證 PageHelper 方法後面跟著 MyBatis 查詢方法,它就是安全的。 因為 PageHelper 自動清除了 ThreadLocal 儲存的物件 Finally.
從上圖中我們可以發現,當乙個請求來臨時,它會獲取儲存當前請求的執行緒的 threadlocal,並呼叫本地頁面get() 來檢查當前執行緒是否有未執行的分頁配置,然後使用 setLocalPage(page) 方法設定執行緒的分頁配置。
@override public object intercept(invocation invocation) throws throwable else checkdialectexists();list resultlist;呼叫該方法以確定是否需要分頁,如果不需要,則返回結果 if (!)。dialect.skip(ms, parameter, rowbounds)) resultlist = executorutil.pagequery(dialect, executor, ms, parameter, rowbounds, resulthandler, boundsql, cachekey); else return dialect.afterpage(resultlist, parameter, rowbounds); finally }
我們需要注意的是mybatis什麼時候使用這個threadlocal,也就是什麼時候獲取到分頁引數?
如前所述,當程式執行 SQL 介面對映器方法時,它將被 ***PageInterceptor 攔截。
PageHelper 其實是 Mybatis 的乙個分頁外掛程式,它的實現原理是通過 PageInterceptor 實現分頁,我們只關注 intercept 方法。
這裡使用 skip 方法設定分頁引數,內部呼叫方法:
page page = pageparams.getpage(parameterobject, rowbounds);
繼續跟蹤 getpage(),發現此方法的第一行獲取 threadlocal 的值:
page page = pagehelper.getlocalpage();
resultlist = executorutil.pagequery(dialect, executor, ms, parameter, rowbounds, resulthandler, boundsql, cachekey);
這就是分頁方法,它根據我們通過 threadlocal 獲取的頁面,決定在執行分頁之前是否進行分頁。
resultlist = executor.query(ms, parameter, rowbounds, resulthandler, cachekey, boundsql);
這是乙個非分頁方法,我們可以想一想,如果執行緒負載在使用後沒有被清除,那麼當執行非分頁方法時,那麼限制就會被串聯到SQL的後面。 為什麼即使你不分割它也不必拼接它? 讓我們回顧一下前面提到的方言skip(ms, parameterobject, rowbounds):
如上圖所示,只要獲取到頁面,SQL 就會使用 executorutilPageQuery 分頁邏輯,最終導致不可預見的情況。
事實上,pagehelper 對分頁後的 threalocal 有明確的處理。
在 intercept 方法結束時,執行 SQL 方法後清除頁面快取
看看這個 afterall() 方法:
只需關注 clearpage():
總的來說,似乎不會有任何問題,但我們可以考慮極端情況的集中:
如果使用startpage(),但是對應的SQL沒有執行,那麼就說明當前執行緒threadlocal有分頁引數,但是沒有使用,下次請求使用這個執行緒的時候就會出現問題。
如果在程式執行 SQL 之前發生異常,則無法執行 FINALLYclearpage()方法,這也會導致執行緒的 ThreadLocal 被汙染。
所以,官方給我們的建議是,在使用 PageHelper 進行分頁時,SQL 執行的 ** 後面應跟 startpage() 方法
除此之外,我們可以在相關方法存在之前手動呼叫 clearpage() 方法。
1.請確保在 mybatis 查詢方法之後立即呼叫 pagehelper 方法,並且在查詢之前不要編寫任何邏輯處理,因為任何 ** 都可能產生異常並導致執行緒重用問題。
2.如果不合理的 ** 原件太多,沒有辦法一一修改,可以考慮在控制器層新增切片,在 jsf 介面中新增過濾器,手動呼叫 clearpage() 方法。 **示例如下:
filter@slf4jpublic類 BSCJSFASPECTFORPAGEHELPER for JSF 介面擴充套件了 AbstractFilter @override public responseMessage invoke(requestMessage requestMessage) catch (exception e) return getnext()。invoke(requestmessage);XML 配置
Slice for controller@aspect@component@slf4jpublic 類 bscaspectforpagehelper @before("bscaspectforpagehelper()") public void dobefore(joinpoint joinpoint) catch (exception e) }
作者:京東物流 王松.
*:京東雲開發者社群 自猿齊說技術 **請註明**。