MyBatis游標(biāo)Cursor的正確使用和百萬數(shù)據(jù)傳輸?shù)膬?nèi)存測試
很早以前為了處理大量數(shù)據(jù)想過使用Cursor,當(dāng)時發(fā)現(xiàn)沒有效果,就沒有繼續(xù)深入。
這次為了搞清楚 Cursor 是否真的有用,找些資料和源碼發(fā)現(xiàn)是有效果的,只是缺了必要的配置。
準(zhǔn)備測試數(shù)據(jù)
創(chuàng)建表:
CREATE TABLE test_table ( id INT PRIMARY KEY, name VARCHAR(20), age INT, address VARCHAR(200) );
創(chuàng)建存儲過程:
-- 創(chuàng)建一個存儲過程,用于插入10萬測試數(shù)據(jù) DELIMITER // CREATE PROCEDURE insert_test_data() BEGIN DECLARE i INT DEFAULT 1; WHILE i <= 100000 DO -- 隨機生成姓名和年齡 SET @name = CONCAT('name', i); SET @address = CONCAT('address......................', i); SET @age = FLOOR(RAND() * 100); -- 插入數(shù)據(jù) INSERT INTO test_table (id, name, age, address) VALUES (i, @name, @age, @address); -- 更新計數(shù)器 SET i = i + 1; END WHILE; END // DELIMITER ;
插入數(shù)據(jù):
-- 調(diào)用存儲過程 CALL insert_test_data();
準(zhǔn)備測試接口
public interface TestMapper { class Person { private String name; private int age; private Integer id; private String address; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } } //TODO 注意sql中指定了表名 test,如果自己執(zhí)行,需要按需修改 @Select("select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table") @Options(fetchSize = Integer.MIN_VALUE) Cursor<Person> selectAll(); @Select("select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table") List<Person> selectList(); }
前面插入10萬數(shù)據(jù),這里union all 10次達(dá)到百萬數(shù)據(jù)。
測試代碼
@Test public void testCursor() throws InterruptedException { //等待10秒方便jvisualVM監(jiān)控 Thread.sleep(10000); long start = System.currentTimeMillis(); try (SqlSession sqlSession = getSqlSession()) { TestMapper testMapper = sqlSession.getMapper(TestMapper.class); try(Cursor<TestMapper.Person> cursor = testMapper.selectAll()){ int total = 0; for (TestMapper.Person o : cursor) { total++; } System.out.println("總數(shù): " + total); } catch (IOException ignore) { } } System.out.println("耗時: " + (System.currentTimeMillis() - start)); Thread.sleep(10000); } @Test public void testSelectAll() throws InterruptedException { //等待10秒方便jvisualVM監(jiān)控 Thread.sleep(10000); long start = System.currentTimeMillis(); try (SqlSession sqlSession = getSqlSession()) { TestMapper testMapper = sqlSession.getMapper(TestMapper.class); List<TestMapper.Person> people = testMapper.selectList(); System.out.println(people.size()); } System.out.println("耗時: " + (System.currentTimeMillis() - start)); Thread.sleep(10000); } private static SqlSessionFactory sqlSessionFactory; @BeforeAll public static void init() { try { Reader reader = Resources.getResourceAsReader("mybatis-config.xml"); sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); reader.close(); } catch (IOException ignore) { ignore.printStackTrace(); } } public SqlSession getSqlSession() { return sqlSessionFactory.openSession(); }
測試結(jié)果
1.1. 直接List接收100萬數(shù)據(jù)
- 查詢過程耗時:7833ms
- GC:21次
- 占用內(nèi)存:885MB
1.2. 限制500MB內(nèi)存,直接List接收100萬數(shù)據(jù)
增加JVM參數(shù) -Xmx500m
執(zhí)行結(jié)果如下:
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.StringCoding$StringDecoder.decode(StringCoding.java:149)
內(nèi)存溢出。
2.1. 使用游標(biāo)Cursor,不配置其他參數(shù)
@Select("select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table") Cursor<Person> selectAll();
- 查詢過程耗時:5908ms
- GC:21次
- 占用內(nèi)存:428MB
使用游標(biāo)的情況在測試中,占用了第1種情況一半的內(nèi)存,處理速度也更快,GC次數(shù)也沒增加。
2.2. 使用游標(biāo)Cursor,不配置其他參數(shù),限制200MB內(nèi)存
等了1分30秒都沒出結(jié)果,而且線程卡在MySQL傳輸數(shù)據(jù)上:
at java.io.FilterInputStream.read(FilterInputStream.java:133)
at com.mysql.cj.protocol.FullReadInputStream.readFully(FullReadInputStream.java:64)
at com.mysql.cj.protocol.a.SimplePacketReader.readMessageLocal(SimplePacketReader.java:137)
at com.mysql.cj.protocol.a.SimplePacketReader.readMessage(SimplePacketReader.java:102)
3.1. 使用游標(biāo)Cursor,配置 FORWARD_ONLY
@Select("select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table union all " + "select * from test.test_table") @Options(resultSetType = ResultSetType.FORWARD_ONLY) Cursor<Person> selectAll();
- 查詢過程耗時:6313ms
- GC:22次
- 占用內(nèi)存:454MB
加了這個參數(shù)不如2.1不配置參數(shù)的情況。
3.2. 使用游標(biāo)Cursor,配置 FORWARD_ONLY,限制200MB內(nèi)存
仍然內(nèi)存溢出:
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.sql.SQLException: GC overhead limit exceeded
以上測試說明 @Options(resultSetType = ResultSetType.FORWARD_ONLY) 配置沒用。
從 MyBatis 源碼來看,就沒有相關(guān)的代碼,不起作用是正常的,但是奇怪的是,網(wǎng)上搜的大量文章都是加的這個配置。
接下來看看真正有用的配置。
4.1. 使用游標(biāo)Cursor,配置 @Options(fetchSize = Integer.MIN_VALUE)
- 查詢過程耗時:4735ms
- GC:12次
- 占用內(nèi)存:206MB
這種情況比前面的都好,而且GC只有12次,內(nèi)存比3.1少了一半。
4.2. 使用游標(biāo)Cursor,配置 @Options(fetchSize = Integer.MIN_VALUE),內(nèi)存限制50MB
- 查詢過程耗時:4676ms
- GC:142次
- 占用內(nèi)存:16MB
16MB內(nèi)存就能處理百萬數(shù)據(jù),但是GC增加了,GC耗時231ms。
4.3. 使用游標(biāo)Cursor,配置 @Options(fetchSize = Integer.MIN_VALUE),內(nèi)存限制10MB
- 查詢過程耗時:38715ms
- GC:1894次
- 占用內(nèi)存:7.8MB
- 16MB內(nèi)存就能處理百萬數(shù)據(jù),但是GC增加了,GC耗時34s。
程序一共運行了39秒,其中34秒是GC時間,吞吐量只有13%,太低了,限制50MB時使用了16MB,增加一次限制20MB的測試。
4.4. 使用游標(biāo)Cursor,配置 @Options(fetchSize = Integer.MIN_VALUE),內(nèi)存限制20MB
- 查詢過程耗時:4880ms
- GC:366次
- 占用內(nèi)存:7.8MB
- 16MB內(nèi)存就能處理百萬數(shù)據(jù),但是GC增加了,GC耗時514ms,吞吐量90%。
正確使用MyBatis游標(biāo)
從上面結(jié)果來看,真正有效的是 @Options(fetchSize = Integer.MIN_VALUE) 配置。
如果追查到JDBC層,會在 mysql 的 jdbc 驅(qū)動StatementImpl類中發(fā)現(xiàn)下面的方法:
/** * We only stream result sets when they are forward-only, read-only, and the * fetch size has been set to Integer.MIN_VALUE * * @return true if this result set should be streamed row at-a-time, rather * than read all at once. */ protected boolean createStreamingResultSet() { return ((this.query.getResultType() == Type.FORWARD_ONLY) && (this.resultSetConcurrency == java.sql.ResultSet.CONCUR_READ_ONLY) && (this.query.getResultFetchSize() == Integer.MIN_VALUE)); }
我們加的注解中,fetchSize條件滿足了,另外兩個在何時設(shè)置的呢?
在AbstractQuery中,存在下面的默認(rèn)值:
protected Resultset.Type resultSetType = Type.FORWARD_ONLY;
在 ConnectionImpl 中的下面方法也有默認(rèn)參數(shù):
@Override public java.sql.PreparedStatement prepareStatement(String sql) throws SQLException { return prepareStatement(sql, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY); }
所以在 MySQL 中,啟用流式傳輸就需要 @Options(fetchSize = Integer.MIN_VALUE) 配置。
當(dāng)考慮到更多類型的數(shù)據(jù)庫時,fetchSize 一般都有不同大小的默認(rèn)值,像 MySQL 這樣直接用 Integer.MIN_VALUE 的不多見,Type.FORWARD_ONLY 也是一些數(shù)據(jù)庫的默認(rèn)值,為了保險可以設(shè)置上,就目前的游標(biāo)功能來看,針對不同的數(shù)據(jù)庫要做對應(yīng)的測試才能找到合適的參數(shù)配置。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Mybatis高級映射、動態(tài)SQL及獲得自增主鍵的解析
MyBatis 本是apache的一個開源項目iBatis, 2010年這個項目由apache software foundation 遷移到了google code,并且改名為MyBatis。這篇文章主要介紹了Mybatis高級映射、動態(tài)SQL及獲得自增主鍵的相關(guān)資料,需要的朋友可以參考下2016-11-11Java中File、Base64、MultipartFile之間相互轉(zhuǎn)換的代碼詳解
File、Base64和MultipartFile都是在編程中常用的類或者數(shù)據(jù)類型,用于處理文件和數(shù)據(jù)的存儲、傳輸和轉(zhuǎn)換等操作,本文將給大家介紹了Java中File、Base64、MultipartFile之間相互轉(zhuǎn)換,文中有詳細(xì)的代碼示例供大家參考,需要的朋友可以參考下2024-04-04MyBatis中的collection兩種使用方法及效率比較
collection主要是應(yīng)對表關(guān)系是一對多的情況,本文主要介紹了MyBatis中的collection兩種使用方法及效率比較,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06解決Process.getInputStream()阻塞的問題
這篇文章主要介紹了解決Process.getInputStream()阻塞的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06Java網(wǎng)絡(luò)編程實現(xiàn)的簡單端口掃描器示例
這篇文章主要介紹了Java網(wǎng)絡(luò)編程實現(xiàn)的簡單端口掃描器,涉及Java網(wǎng)絡(luò)編程Socket組建、swing組建及多線程相關(guān)操作技巧,需要的朋友可以參考下2018-04-04Spring BeanPostProcessor源碼示例解析
這篇文章主要為大家介紹了Spring BeanPostProcessor源碼示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Mybatis學(xué)習(xí)總結(jié)之mybatis使用建議
這篇文章主要介紹了Mybatis學(xué)習(xí)總結(jié)之mybatis使用建議的相關(guān)資料,非常具有參考借鑒價值,需要的朋友可以參考下2016-05-05