背景
大資料量操作的場景大致如下:
- 資料遷移
- 資料匯出
- 批量處理資料
在實際作業中當指定查詢資料過大時,我們一般使用分頁查詢的方式一頁一頁的將資料放到記憶體處理,但有些情況不需要分頁的方式查詢資料或分很大一頁查詢資料時,如果一下子將資料全部加載出來到記憶體中,很可能會發生OOM(記憶體溢位);而且查詢會很慢,因為框架耗費大量的時間和記憶體去把資料庫查詢的結果封裝成我們想要的物件(物體類),
舉例:在業務系統需要從 MySQL 資料庫里讀取 100w 資料行進行處理,應該怎么做?
做法通常如下:
- 常規查詢: 一次性讀取 100w 資料到 JVM 記憶體中,或者分頁讀取
- 流式查詢: 建立長連接,利用服務端游標,每次讀取一條加載到 JVM 記憶體(多次獲取,一次一行)
- 游標查詢: 和流式一樣,通過 fetchSize 引數,控制一次讀取多少條資料(多次獲取,一次多行)
常規查詢
默認情況下,完整的檢索結果集會將其存盤在記憶體中,在大多數情況下,這是最有效的操作方式,并且由于 MySQL 網路協議的設計,因此更易于實作,
舉例:
假設單表 100w 資料量,一般會采用分頁的方式查詢:
@Mapper
public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
@Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ")
Page<BigDataSearchEntity> pageList(@Param("page") Page<BigDataSearchEntity> page, @Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper);
}
注:該示例使用的 MybatisPlus
該方式比較簡單,如果在不考慮 LIMIT 深分頁優化情況下,估計你的資料庫服務器就噶皮了,或者你能等上幾十分鐘或幾小時,甚至幾天時間檢索資料,
推薦一個開源免費的 Spring Boot 最全教程:
https://github.com/javastacks/spring-boot-best-practice
流式查詢
流式查詢指的是查詢成功后不是回傳一個集合而是回傳一個迭代器,應用每次從迭代器取一條查詢結果,流式查詢的好處是能夠降低記憶體使用,
如果沒有流式查詢,我們想要從資料庫取 100w 條記錄而又沒有足夠的記憶體時,就不得不分頁查詢,而分頁查詢效率取決于表設計,如果設計的不好,就無法執行高效的分頁查詢,因此流式查詢是一個資料庫訪問框架必須具備的功能,
MyBatis 中使用流式查詢避免資料量過大導致 OOM ,但在流式查詢的程序當中,資料庫連接是保持打開狀態的,因此要注意的是:
- 執行一個流式查詢后,資料庫訪問框架就不負責關閉資料庫連接了,需要應用在取完資料后自己關閉,
- 必須先讀取(或關閉)結果集中的所有行,然后才能對連接發出任何其他查詢,否則將引發例外,
MyBatis 流式查詢介面
MyBatis 提供了一個叫 org.apache.ibatis.cursor.Cursor 的介面類用于流式查詢,這個介面繼承了 java.io.Closeable 和 java.lang.Iterable 介面,由此可知:
- Cursor 是可關閉的;
- Cursor 是可遍歷的,
除此之外,Cursor 還提供了三個方法:
- isOpen(): 用于在取資料之前判斷 Cursor 物件是否是打開狀態,只有當打開時 Cursor 才能取資料;
- isConsumed(): 用于判斷查詢結果是否全部取完,
- getCurrentIndex(): 回傳已經獲取了多少條資料
使用流式查詢,則要保持對產生結果集的陳述句所參考的表的并發訪問,因為其 查詢會獨占連接,所以必須盡快處理
為什么要用流式查詢?
如果有一個很大的查詢結果需要遍歷處理,又不想一次性將結果集裝入客戶端記憶體,就可以考慮使用流式查詢;
分庫分表場景下,單個表的查詢結果集雖然不大,但如果某個查詢跨了多個庫多個表,又要做結果集的合并、排序等動作,依然有可能撐爆記憶體;詳細研究了sharding-sphere的代碼不難發現,除了group by與order by欄位不一樣之外,其他的場景都非常適合使用流式查詢,可以最大限度的降低對客戶端記憶體的消耗,
游標查詢
對大量資料進行處理時,為防止記憶體泄漏情況發生,也可以采用游標方式進行資料查詢處理,這種處理方式比常規查詢要快很多,
當查詢百萬級的資料的時候,還可以使用游標方式進行資料查詢處理,不僅可以節省記憶體的消耗,而且還不需要一次性取出所有資料,可以進行逐條處理或逐條取出部分批量處理,一次查詢指定 fetchSize 的資料,直到把資料全部處理完,
Mybatis 的處理加了兩個注解:@Options 和 @ResultType
@Mapper
public interface BigDataSearchMapper extends BaseMapper<BigDataSearchEntity> {
// 方式一 多次獲取,一次多行
@Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000000)
Page<BigDataSearchEntity> pageList(@Param("page") Page<BigDataSearchEntity> page, @Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper);
// 方式二 一次獲取,一次一行
@Select("SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 100000)
@ResultType(BigDataSearchEntity.class)
void listData(@Param(Constants.WRAPPER) QueryWrapper<BigDataSearchEntity> queryWrapper, ResultHandler<BigDataSearchEntity> handler);
}
@Options
ResultSet.FORWORD_ONLY:結果集的游標只能向下滾動ResultSet.SCROLL_INSENSITIVE:結果集的游標可以上下移動,當資料庫變化時,當前結果集不變ResultSet.SCROLL_SENSITIVE:回傳可滾動的結果集,當資料庫變化時,當前結果集同步改變fetchSize:每次獲取量
@ResultType
@ResultType(BigDataSearchEntity.class):轉換成回傳物體型別
注意:回傳型別必須為 void ,因為查詢的結果在
ResultHandler里處理資料,所以這個 hander 也是必須的,可以使用 lambda 實作一個依次處理邏輯,
注意:
雖然上面的代碼中都有 @Options 但實際操作卻有不同:
- 方式一是多次查詢,一次回傳多條;
- 方式二是一次查詢,一次回傳一條;
原因:
Oracle 是從服務器一次取出 fetch size 條記錄放在客戶端,客戶端處理完成一個批次后再向服務器取下一個批次,直到所有資料處理完成,
MySQL 是在執行 ResultSet.next() 方法時,會通過資料庫連接一條一條的回傳,flush buffer 的程序是阻塞式的,如果網路中發生了擁塞,send buffer 被填滿,會導致 buffer 一直 flush 不出去,那 MySQL 的處理執行緒會阻塞,從而避免資料把客戶端記憶體撐爆,
非流式查詢和流式查詢區別:
- 非流式查詢:記憶體會隨著查詢記錄的增長而近乎直線增長,
- 流式查詢:記憶體會保持穩定,不會隨著記錄的增長而增長,其記憶體大小取決于批處理大小
BATCH_SIZE的設定,該尺寸越大,記憶體會越大,所以BATCH_SIZE應該根據業務情況設定合適的大小,
另外要切記每次處理完一批結果要記得釋放存盤每批資料的臨時容器,即上文中的gxids.clear();
著作權宣告:本文為CSDN博主「曠野歷程」的原創文章,遵循CC 4.0 BY-SA著作權協議,轉載請附上原文出處鏈接及本宣告,原文鏈接:https://blog.csdn.net/xhaimail/article/details/119386460
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
2.勁爆!Java 協程要來了,,,
3.Spring Boot 2.x 教程,太全了!
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/545528.html
標籤:Java
