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

    1.启服务器.先server.js,再命令行输入命令node server.js,打开浏览器输入http://127.0.0.1:2016可以看到有内容输出. server.js代码如下: var h ...

  2. kafka常用命令

    以下是kafka常用命令行总结: 0.查看有哪些主题: ./kafka-topics.sh --list --zookeeper 192.168.0.201:12181 1.查看topic的详细信息 ...

  3. Node包管理工具

    Node包管理工具 只是简单的介绍一些工具的使用,有利于开发过程.除了介绍Node包管理工具,还介绍了前端打包工具,前端模块管理工具 Node包管理工具:    --npm    --cnpm    ...

  4. c#DataGridView数据绑定示例——格式化单元格的内容(转)

    转自http://blog.csdn.net/testcs_dn/article/details/37834063 c#DataGridView数据绑定示例 格式化单元格的内容 在使用DataGrid ...

  5. 数据持久化以及DAO模式的简单使用

    持久化:(是将程序中的数据在瞬时状态和持久状态间转换机制)        即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘).持久化的主要应用是将内存中的对象存储在关系型的数据库中,当然 ...

  6. 使用 Python 切割图片

    刚好我有张 PNG 图片需要均匀切割,刚好我不会 PhotoShop,刚好我想用 Python 来练练手. 于是撸袖子码脚本. import os from PIL import Image def ...

  7. android 触摸事件分析

    背景知识: 触摸屏可以有多个触控点 android中管理触控点通过一个数组来管理,涉及到index和id两个变量, index表示在数组中的下标,id表示这个触控点(pointer)的id,point ...

  8. Android first --- 网络编程

    网络编程 ###图片下载查看 1.发送http请求 URL url = new URL(address); //获取连接对象,并没有建立连接 HttpURLConnection conn = (Htt ...

  9. Mysql Specified key was too long; max key length is 767 bytes

    今天导入一个数据库时,看到以下报错信息: Specified key was too bytes 直译就是索引键太长,最大为767字节. 查看sql库表文件,发现有一列定义如下: 列   名:cont ...

  10. 关于leetcode中链表中两数据相加的程序说明

    * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ ...