MySQL JDBC/MyBatis Stream方式读取SELECT超大结果集

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

时间: 2024-10-14 07:48:52

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

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

原文链接:当执行一条 select 语句时,MySQL 到底做了啥? 也许,你也跟我一样,在遇到数据库问题时,总时茫然失措,想重启解决问题,又怕导致数据丢失,更怕重启失败,影响业务. 就算重启成功了,对于问题的原因仍不知所以. 本文开始,记录学习<MySQL实战45讲>专栏的过程. 也许有人会问,你记录有什么意义?直接看专栏不就行了吗?你这不是啃别人的剩骨头吗? 是的,这个系列,我只是基于专栏学习,但是我会尽量从我的角度搞懂每一个知识点,遇到不懂得也会将知识点进行拆分. 我知道关注公众号的小伙

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

项目需要,继续学习springmvc,这里加入Mybatis对数据库的访问,并写下一个简单的例子便于以后学习,希望对看的人有帮助.上一篇被移出博客主页,这一篇努力排版整齐,更原创,希望不要再被移出主页了. 原创文章,后面附上源码,转载请注明出处http://www.cnblogs.com/lin557/p/6179618.html 一.运行环境 Eclipse Neon.1a Release (4.6.1) 官网下载 mysql-5.7.16-winx64(http://cdn.mysql.co

JDBC纯驱动方式连接MySQL

1 新建一个名为MysqlDemo的JavaProject 2 从http://dev.mysql.com/downloads/connector/j/中下载最新的驱动包. 这里有.tar.gz和.zip两种格式的包,因为在windows下都可以解压缩,随便下一个都行. 3 将下载的驱动包解压缩后,将MySQL-connector-Java-5.1.38-bin.jar拷贝到项目中 4 在项目中建立一个名为MysqlDemo的Java类 5 在MysqlDemo.java中编写代码 [java]

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

连接数据库步骤: 1. 注册驱动(只做一次) 2. 建立连接(Connection) 3. 创建执行SQL的语句(Statement) 4. 执行语句 5. 处理执行结果(ResultSet) 6. 释放资源举例:public void connectionOracle() throws SQLException{ Connection conn = null; // 数据库连接 PreparedStatement ps = null; // 预编译语句对象 ResultSet rs = nul

MySQL、Oracle、Sql Server数据库JDBC的连接方式

MySQL: 先添加MySQL的jar包 String url="jdbc:mysql://localhost:3306/数据库名";       //数据库地址 String name="root";       //数据库用户名 String password="123456";       //数据库用户密码 Class.forName("com.mysql.jdbc.Driver") ;      //加载MySQL驱

MySql &amp; JDBC

1.什么是数据库? 数据库就是存储数据的仓库,其本质是一个文件系统,数据按照特定的格式将数据存储起来,用户可以通过SQL对数据库中的数据进行增加.修改.删除.及查询操作. 数据库系统类型(历史发展): 网状型数据库 层次型数据库 关系数据库 ---理论最成熟.应用最广泛 面向对象数据库 常见的数据库(软件): MYSQL Oracle DB2 SQLServer SyBase SQLite Java相关: MYSQL  Oracle 2.数据库和表 数据表示存储数据的逻辑单元,可以把数据表想象成

MySQL JDBC 出现多个 SHOW VARIABLES 语句。

一次偶然的机会,show processlist 的时候,发现有个 Client 一直在执行  "mysql-connector-java-5.1.21 ( Revision: ${bzr.revision-id} ) */SHOW VARIABLES WHERE Variable_name" 后面和基友一起讨论,稍微缕了一下.大概思路是这样的的: 1.MySQL JDBC 连接过程大概如下(开启 general log 获得的信息): 5089492 Connect [email p

presto集群安装&整合hive|mysql|jdbc

Presto是一个运行在多台服务器上的分布式系统. 完整安装包括一个coordinator(调度节点)和多个worker. 由客户端提交查询,从Presto命令行CLI提交到coordinator. coordinator进行解析,分析并执行查询计划,然后分发处理队列到worker中. 目录: 搭建前环境准备 集群计划 连接器 安装步骤 配置文件 运行presto 整合hive测试 整合mysql测试 整合jdbc测试 1.搭建前环境准备 CentOS 6.7 java8 Python3.4.4

正确使用MySQL JDBC setFetchSize

MYSQL JDBC快速查询响应的方法,快速返回机制的实现 一直很纠结,Oracle的快速返回机制,虽然结果集很多,可是它能很快的显示第一个结果,虽然通过MYSQl的客户端可以做到,但是通过JDBC却不行. 今天用了1个多小时,终于搞定此问题,希望对广大Java朋友在处理数据库时有个参考. 来由: 通过命令行客户端加上-q参数,可以极快的响应一个查询.    比如结果集为几千万的select * from t1,完整结果集需要20秒,通过-q参数显示第一行只需要不到1秒.    但通过jdbc进