MyBatis游标Cursor的正确用法和百万数据传输的内存测试

这篇具有很好参考价值的文章主要介绍了MyBatis游标Cursor的正确用法和百万数据传输的内存测试。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

很早以前为了处理大量数据想过使用Cursor,当时发现没有效果,就没有继续深入。这次为了搞清楚 Cursor 是否真的有用,找些资料和源码发现是有效果的,只是缺了必要的配置。

准备测试数据

创建表:

CREATE TABLE test_table (
  id INT PRIMARY KEY,
  name VARCHAR(20),
  age INT,
	address VARCHAR(200)
);

创建存储过程:

-- 创建一个存储过程,用于插入10万测试数据
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);
    -- 插入数据
    INSERT INTO test_table (id, name, age, address) VALUES (i, @name, @age, @address);
    -- 更新计数器
    SET i = i + 1;
  END WHILE;
END //
DELIMITER ;

插入数据:

-- 调用存储过程
CALL insert_test_data();

准备测试接口

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,如果自己执行,需要按需修改
    @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万数据,这里union all 10次达到百万数据。

测试代码

@Test
public void testCursor() throws InterruptedException {
    //等待10秒方便jvisualVM监控
    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("总数: " + total);
        } catch (IOException ignore) {
        }
    }
    System.out.println("耗时: " + (System.currentTimeMillis() - start));
    Thread.sleep(10000);
}


@Test
public void testSelectAll() throws InterruptedException {
    //等待10秒方便jvisualVM监控
    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();
}

测试结果

1.1. 直接List接收100万数据

mybatis cursor,MySQL,Mybatis,mybatis,sql,mysql,流式传输,百万数据

查询过程耗时:7833ms
GC:21次
占用内存:885MB

1.2. 限制500MB内存,直接List接收100万数据

增加JVM参数 -Xmx500m
mybatis cursor,MySQL,Mybatis,mybatis,sql,mysql,流式传输,百万数据

执行结果如下:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.lang.StringCoding$StringDecoder.decode(StringCoding.java:149)

内存溢出。

2.1. 使用游标Cursor,不配置其他参数

@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();

mybatis cursor,MySQL,Mybatis,mybatis,sql,mysql,流式传输,百万数据

查询过程耗时:5908ms
GC:21次
占用内存:428MB

使用游标的情况在测试中,占用了第1种情况一半的内存,处理速度也更快,GC次数也没增加。

2.2. 使用游标Cursor,不配置其他参数,限制200MB内存

mybatis cursor,MySQL,Mybatis,mybatis,sql,mysql,流式传输,百万数据
等了1分30秒都没出结果,而且线程卡在MySQL传输数据上:

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. 使用游标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();

mybatis cursor,MySQL,Mybatis,mybatis,sql,mysql,流式传输,百万数据

查询过程耗时:6313ms
GC:22次
占用内存:454MB

加了这个参数不如2.1不配置参数的情况。

3.2. 使用游标Cursor,配置 FORWARD_ONLY,限制200MB内存

mybatis cursor,MySQL,Mybatis,mybatis,sql,mysql,流式传输,百万数据
仍然内存溢出:

org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: java.sql.SQLException: GC overhead limit exceeded

以上测试说明 @Options(resultSetType = ResultSetType.FORWARD_ONLY) 配置没用。

从 MyBatis 源码来看,就没有相关的代码,不起作用是正常的,但是奇怪的是,网上搜的大量文章都是加的这个配置。

接下来看看真正有用的配置。

4.1. 使用游标Cursor,配置 @Options(fetchSize = Integer.MIN_VALUE)

mybatis cursor,MySQL,Mybatis,mybatis,sql,mysql,流式传输,百万数据

查询过程耗时:4735ms
GC:12次
占用内存:206MB

这种情况比前面的都好,而且GC只有12次,内存比3.1少了一半。

4.2. 使用游标Cursor,配置 @Options(fetchSize = Integer.MIN_VALUE),内存限制50MB

mybatis cursor,MySQL,Mybatis,mybatis,sql,mysql,流式传输,百万数据

查询过程耗时:4676ms
GC:142次
占用内存:16MB

16MB内存就能处理百万数据,但是GC增加了,GC耗时231ms。

4.3. 使用游标Cursor,配置 @Options(fetchSize = Integer.MIN_VALUE),内存限制10MB

mybatis cursor,MySQL,Mybatis,mybatis,sql,mysql,流式传输,百万数据

查询过程耗时:38715ms
GC:1894次
占用内存:7.8MB
16MB内存就能处理百万数据,但是GC增加了,GC耗时34s。

程序一共运行了39秒,其中34秒是GC时间,吞吐量只有13%,太低了,限制50MB时使用了16MB,增加一次限制20MB的测试。

4.4. 使用游标Cursor,配置 @Options(fetchSize = Integer.MIN_VALUE),内存限制20MB

mybatis cursor,MySQL,Mybatis,mybatis,sql,mysql,流式传输,百万数据

查询过程耗时:4880ms
GC:366次
占用内存:7.8MB
16MB内存就能处理百万数据,但是GC增加了,GC耗时514ms,吞吐量90%。

正确使用MyBatis游标

从上面结果来看,真正有效的是 @Options(fetchSize = Integer.MIN_VALUE) 配置。

如果追查到JDBC层,会在 mysql 的 jdbc 驱动StatementImpl类中发现下面的方法:

/**
 * 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条件满足了,另外两个在何时设置的呢?

AbstractQuery中,存在下面的默认值:

protected Resultset.Type resultSetType = Type.FORWARD_ONLY;

ConnectionImpl 中的下面方法也有默认参数:

@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) 配置。

当考虑到更多类型的数据库时,fetchSize 一般都有不同大小的默认值,像 MySQL 这样直接用 Integer.MIN_VALUE 的不多见,Type.FORWARD_ONLY 也是一些数据库的默认值,为了保险可以设置上,就目前的游标功能来看,针对不同的数据库要做对应的测试才能找到合适的参数配置。文章来源地址https://www.toymoban.com/news/detail-763346.html

到了这里,关于MyBatis游标Cursor的正确用法和百万数据传输的内存测试的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • git stash 正确用法

    目录 一、背景 二、使用 2.1 使用之前,先简单了解下 git stash 干了什么:  2.2 git stash 相关命令 2.3 使用流程 1. 执行 `git stash`  2. 查看刚才保存的工作进度 `git stash list` 3. 这时候在看分支已经是干净无修改的(改动都有暂存到 stash) 4. 现在就可以正常切换到目标分支,进行相应

    2024年02月20日
    浏览(60)
  • 盘点百度的正确用法-持续更新

    在我们的日常生活和工作中,遇到问题时,很多人会选择百度一下。但有时候百度的结果却不是自己想要的,甚至有些人还被百度的广告所误导。百度是个好东西,那我们该如何正确、高效的使用百度帮我们解决问题呢?今天小编给大家讲讲关于百度的知识,希望对你有所帮

    2024年02月05日
    浏览(33)
  • MyBatis进阶:告别SQL注入!MyBatis分页与特殊字符的正确使用方式

    目录 引言 一、使用正确的方式实现分页 1.1.什么是分页 1.2.MyBatis中的分页实现方式 1.3.避免SQL注入的技巧 二、特殊字符的正确使用方式 2.1.什么是特殊字符 2.2.特殊字符在SQL查询中的作用 2.3.如何避免特殊字符引起的问题 2.3.1.使用CDATA区段  2.3.2.使用实体引用 三、总结和展望

    2024年02月11日
    浏览(36)
  • Python函数的正确用法及其注意事项

    简单总结: 与类和实例无绑定关系的function都属于函数(function); 与类和实例有绑定关系的function都属于方法(method)。 首先摒弃错误认知:并不是类中的调用都叫方法 函数(FunctionType) 函数是封装了一些独立的功能,可以直接调用,能将一些数据(参数)传递进去进行处

    2024年01月20日
    浏览(45)
  • Java从List中删除元素的正确用法

    还是先举个例子,你侄女对天文知识感兴趣,然后你就用程序写了太阳系九大星系(水星、金星、地球、火星、木星、土星、天王星、海王星、冥王星)的运行轨迹图,然后拿给侄女看。然后她说错了错了,你的知识太旧了,多了一颗星。根据2006年8月24日国际天文联合大会召

    2024年02月09日
    浏览(40)
  • selenium---浏览器F12的正确用法

    测试过程中经常会进行抓包来查看一些错误内容,判断是前端的问题还是后端的问题,常见的抓包工具有Fiddler,Charles,还有web端的F12。今天安静来介绍下如何通过F12进行抓包查看请求内容 打开百度按下键盘F12或者邮件选择检查,这里可以看到有一些选项:Elements,Console,

    2024年02月09日
    浏览(33)
  • MyBatis SqlSession事务与批量执行正确方式(默认不生效)

    某些情况下会使用MyBatis的SqlSessionFactory.openSession()方法获取SqlSession对象,再进行数据库操作,但默认情况下SqlSession的事务与批量执行均不生效,假如希望使用SqlSession时事务或批量执行能够生效,则需要进行额外的处理 调用org.apache.ibatis.session.SqlSessionFactory接口的以下openSess

    2024年02月09日
    浏览(44)
  • MyBatis@Param注解的用法

    本人在学习mybatis的过程中遇到的一个让人不爽的bug,在查找了些相关的资料后得以解决,遂记录。 mapper 中有一方法: 测试方法: 报错信息: org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter \\\'name\\\' not found. Available parameters are [arg3, arg2

    2024年02月14日
    浏览(29)
  • mybatis-plus用法(二)

    (5条消息) mybatis-plus用法(一)_渣娃工程师的博客-CSDN博客 AR模式 ActiveRecord模式,通过操作实体对象,直接操作数据库表。与ORM有点类似。 示例如下 让实体类 User 继承自 Model 直接调用实体对象上的方法 结果 其他示例 主键策略 在定义实体类时,用 @TableId 指定主键,而其 t

    2024年02月08日
    浏览(34)
  • MyBatis面试题及高级用法

    答案1: MyBatis是一个Java持久层框架,通过将SQL语句映射到对象,简化了数据库访问。它的主要特点包括动态SQL生成、自动参数映射和复杂映射支持。 答案2: MyBatis与大多数对象关系映射(ORM)框架的区别在于,它不需要使用面向对象的查询语言编写数据库查询。相反,MyBatis允

    2024年02月02日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包