Transmisión de grandes conjuntos de resultados con MySQL

Estoy desarrollando una aplicación de spring que utiliza grandes tablas de MySQL. Al cargar tablas grandes, obtengo una OutOfMemoryException , ya que el controlador intenta cargar toda la tabla en la memoria de la aplicación.

Intenté usar

 statement.setFetchSize(Integer.MIN_VALUE); 

pero luego cada ResultSet que abro cuelga en close() ; buscando en línea, descubrí que eso sucede porque intenta cargar las filas no leídas antes de cerrar el ResultSet, pero ese no es el caso, ya que hago esto:

 ResultSet existingRecords = getTableData(tablename); try { while (existingRecords.next()) { // ... } } finally { existingRecords.close(); // this line is hanging, and there was no exception in the try clause } 

Los lockings también suceden para las tablas pequeñas (3 filas), y si no cierro RecordSet (que sucedió en un método), entonces connection.close() cuelga.


Stack trace of hang:

SocketInputStream.socketRead0 (FileDescriptor, byte [], int, int, int) línea: no disponible [método nativo]
SocketInputStream.read (byte [], int, int) línea: 129
Línea ReadAheadInputStream.fill (int): 113
ReadAheadInputStream.readFromUnderlyingStreamIfNecessary (byte [], int, int) línea: 160
ReadAheadInputStream.read (byte [], int, int) línea: 188
Línea MysqlIO.readFully (InputStream, byte [], int, int): 2428 Línea MysqlIO.reuseAndReadPacket (Buffer, int): 2882
Línea MysqlIO.reuseAndReadPacket (Buffer): 2871
Línea MysqlIO.checkErrorPacket (int): 3414
Línea MysqlIO.checkErrorPacket (): 910
MysqlIO.nextRow (Field [], int, boolean, int, boolean, boolean, boolean, Buffer) línea: 1405
Línea RowDataDynamic.nextRecord (): 413
RowDataDynamic.next () línea: 392 RowDataDynamic.close () línea: 170
JDBC4ResultSet (ResultSetImpl) .realClose (boolean) línea: 7473 JDBC4ResultSet (ResultSetImpl) .close () línea: 881 DelegatingResultSet.close () línea: 152
Línea DelegatingResultSet.close (): 152
DelegatingPreparedStatement (DelegatingStatement) .close () línea: 163
(Esta es mi clase) Línea de Database.close (): 84

Solo establecer el tamaño de búsqueda no es el enfoque correcto. El javadoc de Statement#setFetchSize() ya establece lo siguiente:

Le da al controlador JDBC una pista sobre el número de filas que se deben recuperar de la base de datos

El controlador es realmente libre de aplicar o ignorar la sugerencia. Algunos controladores lo ignoran, algunos controladores lo aplican directamente, algunos controladores necesitan más parámetros. El controlador JDBC de MySQL cae en la última categoría. Si revisa la documentación del controlador JDBC de MySQL , verá la siguiente información (desplácese aproximadamente 2/3 hacia abajo hasta el encabezado ResultSet ):

Para habilitar esta funcionalidad, debe crear una instancia de Statement de la siguiente manera:

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

Lea toda la sección del documento, también describe las advertencias de este enfoque. Aquí hay una cita relevante:

Hay algunas advertencias con este enfoque. Tendrá que leer todas las filas del conjunto de resultados (o cerrarlo) antes de poder emitir otras consultas en la conexión, o se lanzará una excepción.

(…)

Si la statement está dentro del scope de una transacción, los lockings se liberan cuando la transacción se completa (lo que implica que la statement debe completarse primero). Al igual que con la mayoría de las otras bases de datos, las declaraciones no se completan hasta que se leen todos los resultados pendientes en la statement o cuando se cierra el conjunto de resultados activos para la statement.

Si eso no soluciona el OutOfMemoryError (no Exception ), entonces es probable que esté almacenando todos los datos en la memoria de Java en lugar de procesarlos inmediatamente tan pronto como ingresen los datos. Esto requeriría más cambios en su código. , tal vez una reescritura completa. He respondido una pregunta similar antes aquí .

No cierre su ResultSet s dos veces.

Aparentemente, al cerrar un Statement , intenta cerrar el ResultSet correspondiente, como se puede ver en estas dos líneas desde el rastro de la stack:

Línea DelegatingResultSet.close (): 152
DelegatingPreparedStatement (DelegatingStatement) .close () línea: 163

Creí que el locking estaba en ResultSet.close() pero en realidad estaba en Statement.close() que llama a ResultSet.close() . Como el ResultSet ya estaba cerrado, simplemente colgó.

Reemplazamos todos los ResultSet.close() con results.getStatement().close() y eliminamos todos los Statement.close() s, y el problema ya está resuelto.

En caso de que alguien tenga el mismo problema, lo resolví utilizando la cláusula LIMIT en mi consulta.

Este problema se informó a MySql como un error (encuéntrelo aquí http://bugs.mysql.com/bug.php?id=42929 ) que ahora tiene el estado “no es un error”. La parte más pertinente es:

No hay forma de que actualmente cierre un conjunto de resultados “midstream”

Como tiene que leer TODAS las filas, tendrá que limitar los resultados de su consulta usando una cláusula como WHERE o LIMIT. Alternativamente, intente lo siguiente:

 ResultSet rs = ... while(rs.next()) { ... if(bailOut == true) { break; } } while(rs.next()); // This will deplete the remaining rows on the stream rs.close(); 

Puede que no sea lo ideal, pero al menos te hace perder el control.

Si está utilizando spring jdbc, entonces necesita usar un creador de estado preparado junto con SimpleJdbcTemplate para establecer fetchSize como Integer.MIN_VALUE. Se describe aquí http://neopatel.blogspot.com/2012/02/mysql-jdbc-driver-and-streaming-large.html

Se cuelga porque incluso si deja de escuchar, la solicitud continúa. Para cerrar ResultSet y Statement en el orden correcto, intente llamar a statement.cancel () primero:

 public void close() { try { statement.cancel(); if (resultSet != null) resultSet.close(); } catch (SQLException e) { // ignore errors on closing } finally { try { statement.close(); } catch (SQLException e) { // ignore errors on closing } finally { resultSet = null; statement = null; } } } 

Scrollable Resultset ignora fetchSize y recupera todas las filas a la vez, lo que causa un error de meory.

Para mí funcionó correctamente al establecer useCursors = true, de lo contrario, el conjunto de resultados desplazable ignora todas las implementaciones de tamaño de búsqueda, en mi caso era 5000 pero el grupo de resultados desplazable obtuvo millones de registros a la vez causando un uso excesivo de memoria. el DB subyacente es MSSQLServer.

jdbc: jtds: sqlserver: // localhost: 1433 / ACS; TDS = 8.0; useCursors = true