情景: 遍历并处理一个大表中的所有数据, 这个表中的数据可能会是千万条或者上亿条, 很多人可能会说用分页limit……但需求本身一次性遍历更加方便, 且Oracle/DB2都有方便的游标机制.

  对DB来说Stream其实也就是我们说的游标(Cursor), MySQL的Stream方式有2种, Client Side Cursor和Server Side Cursor. JDBC默认的方式Client Side Cursor, 没有任何设置的默认情况下JDBC驱动会将select的全部结果都读取到Client Side后再处理, 这样的话当select返回的结果集非常大时将会撑爆Client端的内存, JDBC下就是普通的OOM; 当然用MyBatis之类的ORM也有同样的问题, 因为这些东西都是架构在JDBC之上的.

解决办法:
1. 使用Client Side Cursor
PreparedStatement/Statement的setFetchSize方法设置为Integer.MIN_VALUE或者使用方法Statement.enableStreamingResults(), 其实这个方法和设置Integer.MIN_VALUE一样, 源码如下:

public void enableStreamingResults() throws SQLException {
    synchronized (checkClosed().getConnectionMutex()) {
        this.originalResultSetType = this.resultSetType;
        this.originalFetchSize = this.fetchSize;

setFetchSize(Integer.MIN_VALUE);
        setResultSetType(ResultSet.TYPE_FORWARD_ONLY);
    }
}
    
    在网上查了下这种Client Side Cursor的大概实现, 其实mysql本身并没有FetchSize方法, 它是通过使用CS阻塞方式的网络流控制实现服务端不会一下发送大量数据到客户端撑爆客户端内存, 我认为这种方式非常LOW! 是很明显的"补丁"策略; 这样就会造成一个必然的问题就是如果没有全部读取完ResultSet的结果再执行其他sql, 那么将会影响该连接的缓存, 所以这种方式要求要么读取完ResultSet中的全部数据要么需要自己调用ResultSet.close()方法, 也就是得用try {} finally{ rs.close(); }或者jdk7下的try-with-resources语法, 例如:
try (ResultSet rs = pstmt.executeQuery()) {
    Role role = new Role();
    int i = 0;
    while (rs.next()) {
        try {
            role.setRoleId(rs.getString("roleId"));
            role.setState(rs.getInt("state"));
            role.setMiscData(rs.getString("miscData"));

selectHandler.action(role);
        } catch (Exception ex) {
            logger.error("selectAllRoles error!", ex);
        }
    }
}
    
    常用的ORM MyBatis下, 默认select的结果是一个List<XXXObject>, 这样问题就更明显了, 要将select全部结果放到一个集合中再处理, 那么结果集一大OOM是必然; 经过查询MyBatis资料发现有ResultHandler机制, 就是这样handler:
    sqlSession.select("chenlong.mybatislearn.db.mapper.RoleMapper.findAllRoles", handler);
    但是和JDBC方式一样, MyBatis即便用了ResultHandler也是将所有结果都读到Client Side, 内存一样爆掉, 最后总算发现xml mapper里可以配置select的fetchSize, 按照前面JDBC方式将其配置为Integer.MIN_VALUE即-2147483648就正常了, 如下:
    <select id="findAllRoles" fetchSize="-2147483648" resultType="chenlong.mybatislearn.db.struct.Role">
        SELECT * FROM role
    </select>
    但还有一个问题就是这种方式必须自己ResultSet.close(), 通过扒MyBatis代码发现它已经帮我们做了, 如下

  这样就可以放心的在MyBatis下使用Client Side Cursor了.

2. 使用Server Side Cursor
    MySQL JDBC Driver文档中有这样参数的说明:

useCursorFetch

If connected to MySQL > 5.0.2, and setFetchSize() > 0 on a statement, should that statement use cursor-based fetching to retrieve rows?

Default: false

Since version: 5.0.0

在MyBatis中位置为:
<property name="url" value="jdbc:mysql://localhost:3008/mybatislearn?autoReconnect=true&amp;useCursorFetch=true"/>

实测这种Server Side Cursor执行sql后要等很久才开始返回结果, 而Client Side Cursor几乎是瞬间就开始返回结果; 网上查询后的结果是Server Side Cursor使用MySQL Server端的资源(内存/CPU……)处理Cursor, 这个可能是其原因, 但一旦开始返回结果目测两者差别不大.
    
    两者各有优缺点, 尤其是Client Side Cursor必须自己记得ResultSet.close()否则整个连接将不再可用, 此为大坑, 尤其是有连接池的情况.

  再次也发现MySQL相比其他大型RDBMS的弱点, 这种查询游标遍历本该是标配! 而MySQL用这么LOW的实现, 还需要用户掌握这么多黑魔法……F***

  参考代码如下:

http://files.cnblogs.com/files/logicbaby/MyBatisLearn.zip

MySQL JDBC/MyBatis Stream方式读取SELECT超大结果集的更多相关文章

  1. java.lang.ClassNotFoundException: com.mysql.jdbc.Driver解决方式

    昨天整理桌面的时候将桌面的一堆文件移动到F盘去了,结果导致原来建的一些项目名称所有出现红色感叹号,打开一看,原来是由于我把hibernate的那些jar包移走了.导致user library里那些ja ...

  2. MySQL实战 | 01-当执行一条 select 语句时,MySQL 到底做了啥?

    原文链接:当执行一条 select 语句时,MySQL 到底做了啥? 也许,你也跟我一样,在遇到数据库问题时,总时茫然失措,想重启解决问题,又怕导致数据丢失,更怕重启失败,影响业务. 就算重启成功了, ...

  3. mysql数据库插入数据获取自增主键的三种方式(jdbc PreparedStatement方式、mybatis useGeneratedKeys方式、mybatis selectKey方式)

    通常来说对于mysql数据库插入数据获取主键的方法是采用selectKey的方式,特别是当你持久层使用mybatis框架的时候. 本文除此之外介绍其它两种获取主键的方式. 为了方便描述我们先建一张my ...

  4. 从零开始学JAVA(09)-使用SpringMVC4 + Mybatis + MySql 例子(注解方式开发)

    项目需要,继续学习springmvc,这里加入Mybatis对数据库的访问,并写下一个简单的例子便于以后学习,希望对看的人有帮助.上一篇被移出博客主页,这一篇努力排版整齐,更原创,希望不要再被移出主页 ...

  5. SparkStreaming直连方式读取kafka数据,使用MySQL保存偏移量

    SparkStreaming直连方式读取kafka数据,使用MySQL保存偏移量 1. ScalikeJDBC 2.配置文件 3.导入依赖的jar包 4.源码测试 通过MySQL保存kafka的偏移量 ...

  6. mybatis&plus系列------Mysql的JSON字段的读取和转换

    mybatis&plus系列------Mysql的JSON字段的读取和转换 一. 背景 在平常的开发中,我们可能会有这样的需求: 业务数据在存储的时候,并不是以mysql中的varchar丶 ...

  7. mysql jdbc性能优化之mybatis/callablestatement调用存储过程mysql jdbc产生不必要的元数据查询(已解决,cpu负载减少20%)

    INFO | jvm 1 | 2016/08/25 15:17:01 | 16-08-25 15:17:01 DEBUG pool-1-thread-371dao.ITaskDao.callProce ...

  8. jdbc -- 001 -- 一般方式创建数据库连接(oracle/mysql)

    连接数据库步骤: 1. 注册驱动(只做一次) 2. 建立连接(Connection) 3. 创建执行SQL的语句(Statement) 4. 执行语句 5. 处理执行结果(ResultSet) 6. ...

  9. JDBC纯驱动方式连接MySQL

    1 新建一个名为MysqlDemo的JavaProject 2 从http://dev.mysql.com/downloads/connector/j/中下载最新的驱动包. 这里有.tar.gz和.z ...

随机推荐

  1. c#控制台調用SSIS包互传值

    有时候不仅仅需要在内部执行package包,多数情况下,是需要在外部进行调用,比如,需要一个批处理或者控制台程序进行外部调用SSIS包,而往往这个包所配置的连接字符串是经过加密处理的,所以当外部调用S ...

  2. js判断变量是否等于undefined

    js中判断变量是否等于undefined,不是使用==,而是使用typeof. typeof(featureId)!="undefined"

  3. Mvc 模块化开发

    在Mvc中,标准的模块化开发方式是使用Areas,每一个Area都可以注册自己的路由,使用自己的控件器与视图.但是在具体使用上它有如下两个限制 1.必须把视图文件放到主项目的Areas文件夹下才能生效 ...

  4. git github简单知识

    Git 常用命令 git init here -- 创建本地仓库(repository),将会在文件夹下创建一个 .git 文件夹,.git 文件夹里存储了所有的版本信息.标记等内容 git remo ...

  5. nginx :413 Request Entity Too Large

    nginx出现这个问题的原因是请求实体太长了.一般出现种情况是Post请求时Body内容Post的数据太大了, 如上传大文件过大.POST数据比较多. 处理方法 在nginx.conf增加 clien ...

  6. 。Java中的一些小细节

    1.main方法. ------任何一个Java程序都有一个main方法,它是程序的入口. ------当执行  “ java + 类名 “  这个命令时,JVM就会去加载这个类,并且寻找这个类中的m ...

  7. 微信小程序-视图视图引用

    引用 WXML 提供两种文件引用方式import和include. import import可以在该文件中使用目标文件定义的template,如: 在 item.wxml 中定义了一个叫item的t ...

  8. Entity Framework 数据库初始化四种策略

    策略一:数据库不存在时重新创建数据库 Database.SetInitializer<testContext>(new CreateDatabaseIfNotExists<testC ...

  9. 换个新的思路 代替解压jar包 例证:wechat4j 框架中的templateMsg类

    很多朋友在写java的程序的时候都喜欢用第三方的jar包和框架,有可能遇到jar包中的内容已经跟不上官方开发者文档的更新,导致部分内容出错了,这个时候可能就要放弃这个jar的使用,但是这个jar中的其 ...

  10. 关于phpcms 万一忘记密码怎么破?

    莫慌~海洋小生教你~我也是偷偷学来的,呀哈哈哈! first:............................你就认命吧!哈哈哈... 开玩笑开玩笑! LOOK HERE ↓: 1.在没有安装 ...