Mybatis的Cursor避免OOM異常的方法詳解
Cursor是啥
研究Cursor如何避免OOM異常之前,先了解一下Cursor是啥。
在Mybatis中,有一個(gè)特殊的對象Cursor
,這個(gè)對象的注釋上清晰的說明了,這個(gè)類的用途。
/** * Cursor contract to handle fetching items lazily using an Iterator. * Cursors are a perfect fit to handle millions of items queries that would not normally fits in memory. * If you use collections in resultMaps then cursor SQL queries must be ordered (resultOrdered="true") * using the id columns of the resultMap. * * @author Guillaume Darmont / guillaume@dropinocean.com */
Cursors are a perfect fit to handle millions of items queries that would not normally fits in memory. Cursor非常適合處理通常不適合內(nèi)存的數(shù)百萬項(xiàng)查詢
甚至在說明中還著重的說明了是非常適合的。
這個(gè)類的作用其實(shí)就是為了避免在數(shù)據(jù)庫批量查詢到大數(shù)據(jù)時(shí)導(dǎo)致程序OOM錯(cuò)誤。
如何使用Cursor
在Mybatis中使用Cursor非常簡單,只要在Mapper文件中將方法的返回值設(shè)置成Cursor<T>
即可。
@Select("SELECT * FROM log") Cursor<Log> selectAll();
注意:要是想在SpringBoot中使用Cursor的話,需要下面方式二選一,不然的話使用Cursor會(huì)報(bào)錯(cuò)。
- 手動(dòng)創(chuàng)建SqlSession
- 在調(diào)用Mapper方法的方法上標(biāo)注
@Transactional
事務(wù)注解。
之所以需要額外配置是因?yàn)樵赟pringBoot中,Mybatis的SqlSession生命周期只在Mapper方法中,并且在關(guān)閉SqlSession時(shí),還會(huì)將SqlSession**綁定的Cursor關(guān)閉,**所以就需要延長SqlSession的存活時(shí)間了。
Cursor原理
解析Mapper方法返回值
在Mybatis中,調(diào)用Mapper方法時(shí),會(huì)由MapperProxy
進(jìn)行方法的代理。此時(shí)就會(huì)根據(jù)具體的方法進(jìn)行不同的解析
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) { // 解析方法返回值 Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface); if (resolvedReturnType instanceof Class<?>) { this.returnType = (Class<?>) resolvedReturnType; } else if (resolvedReturnType instanceof ParameterizedType) { this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType(); } else { this.returnType = method.getReturnType(); } this.returnsVoid = void.class.equals(this.returnType); this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray(); // 方法是否返回Cursor類型 this.returnsCursor = Cursor.class.equals(this.returnType); this.returnsOptional = Optional.class.equals(this.returnType); this.mapKey = getMapKey(method); this.returnsMap = this.mapKey != null; this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); this.paramNameResolver = new ParamNameResolver(configuration, method); }
根據(jù)Cursor返回值調(diào)用selectCursor
解析Mapper方法得到返回值后,就會(huì)根據(jù)返回值的類型來決定具體調(diào)用的查詢方法。
public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { // ---------- 其他查詢---------------- case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { // Cursor返回類型 result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; // ---------- 其他查詢---------------- return result; }
構(gòu)建statement
使用上面解析Mapper方法后得到的Sql,從數(shù)據(jù)庫鏈接中創(chuàng)建一個(gè)PreparedStatement
并填充對應(yīng)的參數(shù)值。
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = getConnection(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); return stmt; }
封裝Cursor
在調(diào)用的最后,會(huì)將從數(shù)據(jù)庫得到的ResultSet
以及Mybatis內(nèi)部的ResultSetHandler
封裝成Cursor
對象供用戶使用。
public <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling cursor results").object(mappedStatement.getId()); ResultSetWrapper rsw = getFirstResultSet(stmt); List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); if (resultMapCount != 1) { throw new ExecutorException("Cursor results cannot be mapped to multiple resultMaps"); } ResultMap resultMap = resultMaps.get(0); return new DefaultCursor<>(this, resultMap, rsw, rowBounds); }
為啥能避免內(nèi)存溢出
在討論這個(gè)問題前,我們可以看一下在Mybatis中,Cursor返回值的查詢以及批量查詢的實(shí)際調(diào)用邏輯。
Cursor查詢
@Override protected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql); Statement stmt = prepareStatement(handler, ms.getStatementLog()); Cursor<E> cursor = handler.queryCursor(stmt); stmt.closeOnCompletion(); return cursor; }
批量查詢
@Override public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }
可以對比一下兩個(gè)實(shí)際執(zhí)行的方法,比較明顯的區(qū)別就是在批量搜索中,顯式關(guān)閉了打開的Statement
,而在Cursor查詢中,并沒有關(guān)閉與數(shù)據(jù)庫的連接。歸根結(jié)底就是因?yàn)镃ursor在使用上就是在操作原生的Statement
,故不能在查詢后關(guān)閉。
另外,在批量查詢的handler.query(stmt, resultHandler)
方法中,是獲取本次查詢的所有數(shù)據(jù)后返回的,而這就會(huì)導(dǎo)致在大批量數(shù)據(jù)時(shí)塞爆內(nèi)存導(dǎo)致OOM了。
然而在Cursor查詢中,并不會(huì)獲取全部數(shù)據(jù)后返回,而是根據(jù)用戶操作來獲取對于數(shù)據(jù),自然而然也就不會(huì)塞爆內(nèi)存了。
總結(jié)
類型 | 如何獲取數(shù)據(jù) | 返回值 | 是否關(guān)閉Statement、ResultSet |
---|---|---|---|
Cursor | 用戶自行根據(jù)Cursor迭代器獲取 | Cursor游標(biāo)類型 | 不關(guān)閉,需要一直持有 |
普通搜索 | Mybatis內(nèi)部操控JDBC指針,獲取所有查詢數(shù)據(jù)后返回。 | 具體實(shí)體類型 | 獲取完數(shù)據(jù)后關(guān)閉 |
以上就是Mybatis的Cursor避免OOM異常的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Mybatis Cursor避免OOM異常的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java 學(xué)習(xí)筆記(入門篇)_java的安裝與配置
學(xué)習(xí)Java已經(jīng)很長時(shí)間了,由于基礎(chǔ)不好遇到問題就無從下手,所以,打算寫Java的隨手筆記來鞏固基礎(chǔ),加強(qiáng)學(xué)習(xí),接下來講解java的安裝,配置等,感興趣的朋友可以參考下2013-01-01Spring的@CrossOrigin注解使用與CrossFilter對象自定義詳解
這篇文章主要介紹了Spring的@CrossOrigin注解使用與CrossFilter對象自定義詳解,跨域,指的是瀏覽器不能執(zhí)行其他網(wǎng)站的腳本,它是由瀏覽器的同源策略造成的,是瀏覽器施加的安全限制,所謂同源是指,域名,協(xié)議,端口均相同,需要的朋友可以參考下2023-12-12java 遞歸查詢所有子節(jié)點(diǎn)id的方法實(shí)現(xiàn)
在多層次的數(shù)據(jù)結(jié)構(gòu)中,經(jīng)常需要查詢一個(gè)節(jié)點(diǎn)下的所有子節(jié)點(diǎn),本文主要介紹了java 遞歸查詢所有子節(jié)點(diǎn)id的方法實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03Flink實(shí)現(xiàn)特定統(tǒng)計(jì)的歸約聚合reduce操作
這篇文章主要介紹了Flink實(shí)現(xiàn)特定統(tǒng)計(jì)的歸約聚合reduce操作,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-02-02小米Java程序員第二輪面試10個(gè)問題 你是否會(huì)被刷掉?
小米Java程序員第二輪面試10個(gè)問題,你是否會(huì)被刷掉?掌握好基礎(chǔ)知識,祝大家面試順利2017-11-11Java多線程并發(fā)synchronized?關(guān)鍵字
這篇文章主要介紹了Java多線程并發(fā)synchronized?關(guān)鍵字,Java?在虛擬機(jī)層面提供了?synchronized?關(guān)鍵字供開發(fā)者快速實(shí)現(xiàn)互斥同步的重量級鎖來保障線程安全。2022-06-06詳解SpringBoot構(gòu)建的Web項(xiàng)目如何在服務(wù)端校驗(yàn)表單輸入
這篇文章主要介紹了詳解SpringBoot構(gòu)建的Web項(xiàng)目如何在服務(wù)端校驗(yàn)表單輸入,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10