情景: 遍历并处理一个大表中的所有数据, 这个表中的数据可能会是千万条或者上亿条, 很多人可能会说用分页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. myaudio.duration为null的解决办法

    放在 myaudio.addEventListener("canplay",function(){});中,就可以获取到值.

  2. 移动端页面去掉click点击 背景色变化

    a,input,em,h2{-webkit-tap-highlight-color:rgba(255,0,0,0);}给点击元素加上样式 :-webkit-tap-highlight-color:rg ...

  3. CSS中的浮动

    这是一个重点内容,在做网页布局的时候,经常用到,所以在这里单独将其列出来小结!

  4. HTML标签显示在页面上

    用 <xmp></xmp> 标签包起来,里面的所有文字会原样显示出来 <xmp><P>1</P><div>2</div&g ...

  5. maven创建子项目(适用于多模块管理项目)

    在eclipse或者myeclipse下构建maven项目,该项目由多个子模块组成. 1.创建一个父项目 NEW -->project-->maven-->maven Project ...

  6. php变量赋值给js

    1 2 3 4 5 6 7 8 $(document).ready(function(){                 <?php $f="'name'"?>    ...

  7. RPM安装MySQL

    # wget http://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.15-1.el6.x86_64.rpm-bundle.tar # tar x ...

  8. Mifare系列4-组成(转)

    文/闫鑫原创转载请注明出处http://blog.csdn.net/yxstars/article/details/38081241 MIFARE集成电路芯片内含EEPROM.RF接口和数字控制单元. ...

  9. Log4j源代码学习

    了解log4j的源代码来源于项目中一次需求,我们想将系统所有的warn日志统一收集到common-warn.log的日志中去,以便于系统对其进行监控处理.于是模拟自动生成的error配置完成了warn ...

  10. My Sql 中要Alter Table的同学请注意!!!

    首先我建议你在对MySQL表做DDL操作时: 1 执行 show processlist 查看,要操作的表(数据库对象)是否处于锁状态 if("未锁定") { 执行DDL语句 }e ...