MySQL数据库游标通常有两种形式:Client Side Cursor(客户端游标)和Server Side Cursor(服务器端游标)。默认情况下,客户端游标会把整个结果集获取到客户端内存中,如果结果集太大,就会引发Out Of Memory错误;而服务器端游标会将结果集缓存在服务器端,客户端从服务器端分批获得结果集。
MySQL默认是使用客户端游标的,因为通常情况下,程序处理的结果集不会特别大,对小结果集使用客户端游标效率更高:结果集一次性传输到客户端,客户端可以自行处理,服务器端也可以为其他客户端提供服务。但是针对大结果集,默认的客户端游标处理方式满足不了要求,针对这种情况,MySQL的Java客户端做了一个特殊的处理。下面参照官方帮助看一下。
By default, ResultSets are completely retrieved and stored in memory. In most cases this is the most efficient way to operate and, due to the design of the MySQL network protocol, is easier to implement. If you are working with ResultSets that have a large number of rows or large values and cannot allocate heap space in your JVM for the memory required, you can tell the driver to stream the results back one row at a time. To enable this functionality, create a Statement instance in the following manner:
stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); stmt.setFetchSize(Integer.MIN_VALUE);
>The combination of a forward-only, read-only result set, with a fetch size of Integer.`MIN_VALUE` serves as a signal to the driver to stream result sets row-by-row. After this, any result sets created with the statement will be retrieved row-by-row.
2
3简单翻译一下:
4> 缺省情况下,结果集会被整个的拉取并保存到客户端内存中。通常情况下,这是这是最有效的方式,可以与MySQL的网络协议完美配合。如果你需要处理一个大数量级的结果集或者你无法在你的JVM中申请足够的Heap内存,你可以告诉驱动使用流方式返回结果集,一次返回一条记录。
5
6> 要开启这个功能,可以按照下面的代码方式创建结果Statement:
7
8> ```java
9stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY,
10 java.sql.ResultSet.CONCUR_READ_ONLY);
11stmt.setFetchSize(Integer.MIN_VALUE);
使用TYPE_FORWARD_ONLY
,CONCUR_READER_ONLY
的组合创建一个Statement,并且指定fetchSize为Integer.MIN_VALUE,这会告诉MySQL的驱动使用流方式处理结果集,一次返回一条记录。
那么Integer.MIN_VALUE是多少呢?看一下定义:
1@Native public static final int MIN_VALUE = 0x80000000;
计算一下,正好是:-2147483648
。这就是之前提到的这个数字的来历,MySQL驱动需要这个数字来明确开启流处理方式。回忆一下之前博文说道的修改方式,其中的fetchSize, resultType都是按照上面帮助中提到的要求进行设置的:
客户端游标在使用中还是有一些限制或者不足的地方的。我们继续看官方帮助文档:
There are some caveats with this approach. You must read all of the rows in the result set (or close it) before you can issue any other queries on the connection, or an exception will be thrown.
The earliest the locks these statements hold can be released (whether they be MyISAM table-level locks or row-level locks in some other storage engine such as InnoDB) is when the statement completes.
If the statement is within scope of a transaction, then locks are released when the transaction completes (which implies that the statement needs to complete first). As with most other databases, statements are not complete until all the results pending on the statement are read or the active result set for the statement is closed.
Therefore, if using streaming results, process them as quickly as possible if you want to maintain concurrent access to the tables referenced by the statement producing the result set.
翻译一下:
使用这种机制有一些事情需要注意:使用同一个数据库链接,你必须读取结果集中的全部记录(或者关掉结果集)才能够执行新的查询语句,否则会有异常抛出。 只有当Statement执行完毕的时候,其持有的锁才能够被释放(无论是MyISAM的表级锁还是其他存储引擎,例如InnoDB,的行级锁)。 如果该Statement位于一个事务当中,只有当事务完成的时候,Statement持有的锁才会被释放。这同时意味着Statement必须先于事务完成。与大多数数据库一样,只有读取了Statement上的所有结果或关闭了语句的活动结果集,Statement才会关闭。 因此,如果要使用流方式处理结果集,一定要记住尽可能快的处理结果集,以保证对产生结果集的表的并发访问能力。
总结一下就是使用流方式的时候,结果集没有处理完或者Statement没有关闭的时候,是有锁存在的,因此处理速度一定要快,否则会影响并发性能。
摘选自:Spring使用POI导出Excel内存溢出问题的解决(续) - 执子之手
参考:SpringBoot 实现 MySQL 百万级数据量导出并避免 OOM 的解决方案