pagehelper踩坑記之分頁亂套問題解決
正文
我們在使用數(shù)據(jù)庫進行查詢時,很多時候會用到分頁展示功能,因此除了像mybatis這樣的完善的orm框架之外,還有pagehelper這樣的插件幫助減輕我們的工作。
pagehelper的實現(xiàn)方式是,不需要我們?nèi)ゾ帉懛猪摯a,只需要調(diào)用一個分頁方法,出來的結(jié)果就是經(jīng)過分頁處理的。一來,我們的xml中的sql編寫就會靈活很多,二來,它可以幫我們規(guī)避各種不同類型的數(shù)據(jù)庫的分頁描述方式。所以,總體來說是個好事。
使用pagehelper遇到的坑說明
現(xiàn)象是這樣的:我們有一個場景是查詢數(shù)據(jù)庫表中的全量記錄返回給第三方,但是突然某一天發(fā)現(xiàn)第三方告警說我們給的數(shù)據(jù)不對了,比如之前會給到200條記錄的,某次只給到了10條記錄。
隨后我們推出了幾個猜想:
1. 第三方系統(tǒng)處理數(shù)據(jù)有bug,漏掉了一些數(shù)據(jù);
2. 數(shù)據(jù)庫被人臨時改掉過,然后又被復原了;
3. 數(shù)據(jù)庫bug,在全量select時可能不返回全部記錄;
其實以上猜想都顯得有點無厘頭,比如數(shù)據(jù)庫怎么可能有這種低級bug?但是人在沒有辦法的情況下只能胡猜一通了。最后終于發(fā)現(xiàn)是pagehelper的原因,因為分頁亂套了,復用了其他場景下的分頁設(shè)置,丟到數(shù)據(jù)庫查詢后返回了10條記錄;
pagehelper的至簡使用方式
本身pagehelper就是一個輔助工具類,所以使用起來一般很簡單。尤其在springboot中,只要引用starter類,依賴就可以滿足了。(如果是其他版本,則可能需要配置下mybatis的intercepter)
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.version}</version>
</dependency>在使用時只需要加上 Page.startPage(pageNum, pageSize) 即可。
public Object getUsers(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<UserEntity> list = userMapper.selectAllWithPage(null);
com.github.pagehelper.Page listWithPage = (com.github.pagehelper.Page) list;
System.out.println("listCnt:" + listWithPage.getTotal());
return list;
}而真正的sql里只需按沒有分頁的樣式寫一下就可以了。
<select id="selectAllWithPage" parameterType="java.util.Map"
resultType="com.my.mvc.app.dao.entity.UserEntity">
select * from t_users
</select>還是很易用的。少去了一些寫死的sql樣例。
pagehelper實現(xiàn)原理簡說
pagehelper不是什么高深的組件,實際上它就是一個mybatis的一個插件或者攔截器。是mybatis在執(zhí)行調(diào)用時,將請求轉(zhuǎn)發(fā)給pagehelper處理,然后由pagehelper包裝分頁邏輯。
// com.github.pagehelper.PageInterceptor#intercept
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于邏輯關(guān)系,只會進入一次
if (args.length == 4) {
//4 個參數(shù)時
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 個參數(shù)時
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
checkDialectExists();
List resultList;
//調(diào)用方法判斷是否需要進行分頁,如果不需要,直接返回結(jié)果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判斷是否需要進行 count 查詢
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查詢總數(shù)
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
//處理查詢總數(shù),返回 true 時繼續(xù)分頁查詢,false 時直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//當查詢總數(shù)為 0 時,直接返回空的結(jié)果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用參數(shù)值,不使用分頁插件處理時,仍然支持默認的內(nèi)存分頁
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
if(dialect != null){
dialect.afterAll();
}
}
}如果沒有分頁邏輯需要處理,和普通的沒什么差別,如果有分頁請求,則會在原來的sql之上套上limit.. offset.. 之類的關(guān)鍵詞。從而完成分頁效果。
為什么pagehelper的分頁會亂套?
現(xiàn)在我們來說說為什么分頁會亂套?原因是 PageHelper.startPage(xx) 的原理是將分頁信息設(shè)置到線程上下文中,然后在隨后的查詢中使用該值,使用完成后就將該信息清除。
/**
* 開始分頁
*
* @param pageNum 頁碼
* @param pageSize 每頁顯示數(shù)量
* @param count 是否進行count查詢
*/
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
return startPage(pageNum, pageSize, count, null, null);
}
/**
* 開始分頁
*
* @param pageNum 頁碼
* @param pageSize 每頁顯示數(shù)量
* @param count 是否進行count查詢
* @param reasonable 分頁合理化,null時用默認配置
* @param pageSizeZero true且pageSize=0時返回全部結(jié)果,false時分頁,null時用默認配置
*/
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//當已經(jīng)執(zhí)行過orderBy的時候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
/**
* 設(shè)置 Page 參數(shù)
*
* @param page
*/
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
// com.github.pagehelper.PageHelper#afterAll
@Override
public void afterAll() {
//這個方法即使不分頁也會被執(zhí)行,所以要判斷 null
AbstractHelperDialect delegate = autoDialect.getDelegate();
if (delegate != null) {
delegate.afterAll();
autoDialect.clearDelegate();
}
clearPage();
}
/**
* 移除本地變量
*/
public static void clearPage() {
LOCAL_PAGE.remove();
}那么什么情況下會導致分頁信息亂套呢?實際上就是線程變量什么情況會被亂用呢?
線程被復用的時候,將可能導致該問題。比如某個請求將某個線程設(shè)置了一個線程變量,然后隨后另一個請求復用了該線程,那么這個變量就被復用過去了。那么什么情況下線程會被復用呢?
一般是線程池、連接池等等。是的,大概就是這么原理了。
分頁問題復現(xiàn)
既然從理論上說明了這個問題,能否穩(wěn)定復現(xiàn)呢?咱們編寫下面的,很快就復現(xiàn)了。
@RestController
@RequestMapping("/hello")
@Slf4j
public class HelloController {
@Resource
private UserService userService;
// 1. 先請求該getUsers接口,將得到異常,pageNum=1, pageSize=1
@GetMapping("getUsers")
@ResponseBody
public Object getUsers(int pageNum, int pageSize) {
return userService.getUsers(pageNum, pageSize);
}
// 2. 多次請求該 getAllActors接口,正常情況下會得到N條全表記錄,但將會偶發(fā)地得到只有一條記錄,現(xiàn)象復現(xiàn)
@GetMapping("getAllActors")
@ResponseBody
public Object getAllActors() {
return userService.getAllActors();
}
}
@Service
@Slf4j
public class UserService {
@Resource
private UserMapper userMapper;
public Object getUsers(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
// 此處強行拋出異常, 使以上 pagehelper 信息得以保存
throw new RuntimeException("exception ran");
}
public Object getAllActors() {
// 正常的全表查詢
List<ActorEntity> list = userMapper.selectAllActors();
return list;
}
}驗證步驟及結(jié)果如下:(數(shù)據(jù)方面,自己隨便找一些表就好了)
// 步驟1: 發(fā)送請求: http://localhost:8081/hello/getUsers?pageNum=1&pageSize=1
// 步驟2: 發(fā)送請求: http://localhost:8081/hello/getAllActors
// 正常時返回
[{"actorId":1,"firstName":"PENELOPE","lastName":null,"lastUpdate":null},{"actorId":2,"firstName":"NICK","lastName":null,"lastUpdate":null},{"actorId":3,"firstName":"ED","lastName":null,"lastUpdate":null},{"actorId":4,"firstName":"JENNIFER","lastName":null,"lastUpdate":null},{"actorId":5,"firstName":"JOHNNY","lastName":null,"lastUpdate":null},{"actorId":6,"firstName":"BETTE","lastName":null,"lastUpdate":null},{"actorId":7,"firstName":"GRACE","lastName":null,"lastUpdate":null},{"actorId":8,"firstName":"MATTHEW","lastName":null,"lastUpdate":null},{"actorId":9,"firstName":"JOE","lastName":null,"lastUpdate":null},{"actorId":10,"firstName":"CHRISTIAN","lastName":null,"lastUpdate":null},{"actorId":11,"firstName":"ZERO","lastName":null,"lastUpdate":null},{"actorId":12,"firstName":"KARL","lastName":null,"lastUpdate":null},{"actorId":13,"firstName":"UMA","lastName":null,"lastUpdate":null},{"actorId":14,"firstName":"VIVIEN","lastName":null,"lastUpdate":null},{"actorId":15,"firstName":"CUBA","lastName":null,"lastUpdate":null},{"actorId":16,"firstName":"FRED","lastName":null,"lastUpdate":null},...
// 出異常時返回
[{"actorId":1,"firstName":"PENELOPE","lastName":null,"lastUpdate":null}]
以上,幾乎都可以復現(xiàn)該現(xiàn)象。實際上該問題由于tomcat的連接池復用導致的,本身和pagehelper關(guān)聯(lián)不是很大,但是在此處卻可能帶來比較大的影響。這也警示我們使用ThreadLocal 時,一定要小心清理,否則將產(chǎn)生難以預料的結(jié)果。而且將很難排查。供諸君參考,更多關(guān)于pagehelper分頁亂套解決的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringCloud 服務(wù)網(wǎng)關(guān)路由規(guī)則的坑及解決
這篇文章主要介紹了SpringCloud 服務(wù)網(wǎng)關(guān)路由規(guī)則的坑及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
簡單談?wù)刯ava中final,finally,finalize的區(qū)別
Java中final、finally、finalize的區(qū)別與用法,困擾了不少學習者,下面我們就這個問題進行一些探討,希望對大家的學習有所幫助。2016-05-05
SpringBoot實現(xiàn)接口文檔自動生成的方法示例
在開發(fā)Web應(yīng)用程序時,接口文檔是非常重要的一環(huán),本文主要介紹了SpringBoot實現(xiàn)接口文檔自動生成的方法示例,具有一定的參考價值,感興趣的可以了解一下2023-10-10
使用SpringSecurity+defaultSuccessUrl不跳轉(zhuǎn)指定頁面的問題解決方法
本人是用springsecurity的新手,今天遇到defaultSuccessUrl不跳轉(zhuǎn)指定頁面的問題,真是頭疼死了,網(wǎng)上找遍了解決方法都解決不了,今天給大家分享使用SpringSecurity+defaultSuccessUrl不跳轉(zhuǎn)指定頁面的問題解決方法,感興趣的朋友一起看看吧2023-12-12

