MySQL 流式查询的用法和坑

引言

本文整理了 MySQL 流式查询一些原理和用法, 包括 MySQL 官方文档对于 ResultSet 流式查询的说明以及很多网友关于 MySQL 流失查询踩坑的说明. 最后给出了解决流式查询的 connection 在未查询完结果集的数据之前又被其他地方使用导致报错的解决方法, 希望能对读者有所帮助.

原文地址: No statements may be issued when any streaming result sets are open and in use on a given connection

欢迎访问我的博客: http://blog.duhbb.com/

报错日志

org.springframework.dao.DataAccessResourceFailureException: PreparedStatementCallback; SQL []; Could not create connection to database server. Attempted reconnect 3 times. Giving up.; nested exception is java.sql.SQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up.
    at org.springframework.jdbc.support.SQLExceptionSubclassTranslator.doTranslate(SQLExceptionSubclassTranslator.java:81)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
    at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1443)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:633)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:669)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:700)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:712)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:763)
    at org.springframework.jdbc.core.JdbcTemplate.queryForList(JdbcTemplate.java:829)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
    at org.springframework.jdbc.core.JdbcTemplate$RowCallbackHandlerResultSetExtractor.extractData(JdbcTemplate.java:1607)
    at org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:679)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:617)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:669)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:694)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:723)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
Caused by: java.sql.SQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up.
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:110)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:73)
    at com.mysql.cj.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:903)
    at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:828)
    at com.mysql.cj.jdbc.ConnectionImpl.handleReconnect(ConnectionImpl.java:2694)
    at com.mysql.cj.NativeSession.invokeReconnectListeners(NativeSession.java:1215)
    at com.mysql.cj.NativeSession.execSQL(NativeSession.java:1067)
    at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:930)
    at com.mysql.cj.jdbc.ClientPreparedStatement.executeQuery(ClientPreparedStatement.java:1003)
    at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
    at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)
    at org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:678)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:617)
    ... 69 common frames omitted
Caused by: java.sql.SQLException: Streaming result set com.mysql.cj.protocol.a.result.ResultsetRowsStreaming@5869a708 is still active. No statements may be issued when any streaming result sets are open and in use on a given connection. Ensure that you have called .close() on any active streaming result sets before attempting more queries.
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
    at com.mysql.cj.jdbc.ConnectionImpl.pingInternal(ConnectionImpl.java:1524)
    at com.mysql.cj.jdbc.ConnectionImpl.connectWithRetries(ConnectionImpl.java:848)
    ... 79 common frames omitted

MySQL 结果集官方解释

原文地址: 6.4 JDBC API Implementation Notes

结果集

默认情况下, ResultSet 被完全检索并存储在内存中. 在大多数情况下, 这是最有效的操作方式, 并且由于 MySQL 网络协议的设计, 更容易实现. 如果您正在使用具有大量行或大值的 ResultSet, 并且无法在 JVM 中为所需的内存分配堆空间, 您可以告诉驱动程序一次将结果流回一行.

Statement 要启用此功能, 请按以下方式创建实例:

stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);

给结果集设置 forward-only, read-only 以及将 fetch size 设置为 Integer.MIN_VALUE, 即表明让 MySQL 以流式传输结果集中的数据. 在此之后, 将逐行检索使用该语句创建的任何结果集.

这种方法有一些注意事项. 您必须先读取结果集中的所有行 (或将其关闭), 然后才能对连接发出任何其他查询, 否则将引发异常.

这些语句持有的锁最早可以被释放 (无论它们是 MyISAM 表级锁还是其他存储引擎中的行级锁, 例如 InnoDB) 是在语句完成时.

如果语句在事务范围内, 则在事务完成时释放锁 (这意味着语句需要先完成). 与大多数其他数据库一样, 在读取语句上所有待处理的结果或关闭语句的活动结果集之前, 语句是不完整的.

因此, 如果使用流式结果, 如果您想保持对生成结果集的语句所引用的表的并发访问, 请尽快处理它们.

另一种选择是每次使用基于游标的流来检索一组行. 这可以通过将连接属性 useCursorFetch 设置为 true, 然后调用 setFetchSize(int) 来设置每次要获取的行数:

conn = DriverManager.getConnection("jdbc:mysql://localhost/?useCursorFetch=true", "user", "s3cr3t");
stmt = conn.createStatement();
stmt.setFetchSize(100);
rs = stmt.executeQuery("SELECT * FROM your_table_here");

原因分析

来自网友的分析

以下错误消息 "流式传输结果集 com.mysql.jdbc.RowDataDynamic@XXXXXX 仍然处于活动状态. 当任何流式传输结果集打开并在给定连接上使用时, 不会发出任何语句. 确保您已调用 .close() 在尝试更多查询之前在任何活动的流式传输结果集上" 来自 MySQL.

原因在于 MySQL 的流模式, Streaming 模式是从 MySQL 获取数据的最快方式.

但是, 流模式有一个限制: 当结果集在 MySQL 连接上以流模式打开时, 您不能使用同一数据库连接来运行其他查询. 这正是上面的错误消息所告诉的. 该问题的解决方案是将 MySQL 连接设置更改为使用 CursorFetch 模式而不是 Streaming 模式.

CursorFetch 是 MySQL 中第二快的读取方法. 不同之处在于, 在 Streaming 模式下, 服务器尝试立即开始将数据发送给消费者, 在 CursorFetch 上, 它首先填充缓冲区, 然后从缓冲区提供数据. 在非常大的数据集上, 在 MySQL 开始以 CursorFetch 模式为您获取数据之前会有一些初始延迟, 但对于任何实际目的而言, 差异通常可以忽略不计.

以上来自: Error-message-Streaming-result-set-com-mysql-jdbc-RowDataDynamic

流式查询与 fetchsize

既然 fetchsize 这么好用, 那 MySQL 直接设一个值, 不就也可以用到缓冲区, 不必每次都将全量结果集装入内存. 但是, 非常遗憾, MySQL 的 JDBC 驱动本质上并不支持设置 fetchsize, 不管设置多大的 fetchsize, JDBC 驱动依然会将 select 的全部结果都读取到客户端后再处理, 这样的话当 select 返回的结果集非常大时将会撑爆 Client 端的内存.

但也不是完全没办法, PreparedStatement/StatementsetFetchSize 方法设置为 Integer.MIN_VALUE 或者使用方法 Statement.enableStreamingResults(), 也可以实现流式查询, 在执行 ResultSet.next() 方法时, 会通过数据库连接一条一条的返回, 这样也不会大量占用客户端的内存.

MySQL 流式查询的坑

其实 MySQL 本身并没有 FetchSize 方法, 它是通过使用 CS 阻塞方式的网络流控制实现服务端不会一下发送大量数据到客户端撑爆客户端内存, 这种实现方式比起商业数据库 Oracle 使用客户端, 服务器端缓冲块暂存查询结果数据来说, 简直是弱爆了! 这样带来的问题: 如果使用了流式查询, 一个 MySQL 数据库连接同一时间只能为一个 ResultSet 对象服务, 并且如果该 ResultSet 对象没有关闭, 势必会影响其他查询对数据库连接的使用! 此为大坑, 难怪 sharding-sphere 费劲心思要提供两种数据库连接模式, 如果应用对数据库连接的消耗要求严苛, 那么流式查询就不再适合.

以上来自: 深入了解 MySQL 的流式查询机制

解决方案

用了 jdbcTemplate 进行流式查询, 然后在流式查询的 callback 中又调用 jdbcTemplate 查询其他相关数据. 生产环境出现了这个问题, 但是本地没有复现.

解决方法: 直接用原始的 jdbc connection 查询, 避免 connection 进行流式查询的时候又被别的地方使用了.

原文地址: No statements may be issued when any streaming result sets are open and in use on a given connection

欢迎访问我的博客: http://blog.duhbb.com/

你可能感兴趣的:(mysqljdbc)