Streaming di grandi set di risultati con MySQL

Sto sviluppando un’applicazione di spring che utilizza grandi tabelle MySQL. Durante il caricamento di tabelle di grandi dimensioni, ottengo OutOfMemoryException , poiché il driver tenta di caricare l’intera tabella nella memoria dell’applicazione.

Ho provato a usare

 statement.setFetchSize(Integer.MIN_VALUE); 

ma poi ogni ResultSet che apro si blocca su close() ; guardando in linea ho scoperto che ciò accade perché prova a caricare le righe non lette prima di chiudere il ResultSet, ma non è il caso dato che faccio questo:

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

Gli hang si verificano anche per le tabelle di piccole dimensioni (3 righe) e, se non si chiude il RecordSet (che si è verificato in un metodo), si blocca connection.close() .


Stack traccia del blocco:

    Linea SocketInputStream.socketRead0 (FileDescriptor, byte [], int, int, int): non disponibile [metodo nativo]
    Linea SocketInputStream.read (byte [], int, int): 129
    Riga ReadAheadInputStream.fill (int): 113
    ReadAheadInputStream.readFromUnderlyingStreamIfNecessario (byte [], int, int) line: 160
    Riga ReadAheadInputStream.read (byte [], int, int): 188
    Linea MysqlIO.readFully (InputStream, byte [], int, int): 2428 Linea MysqlIO.reuseAndReadPacket (Buffer, int): 2882
    Linea MysqlIO.reuseAndReadPacket (Buffer): 2871
    MysqlIO.checkErrorPacket (int) line: 3414
    MysqlIO.checkErrorPacket () line: 910
    MysqlIO.nextRow (Field [], int, boolean, int, boolean, boolean, boolean, Buffer) line: 1405
    Riga RowDataDynamic.nextRecord (): 413
    Riga RowDataDynamic.next (): riga 392 RowDataDynamic.close (): 170
    JDBC4ResultSet (ResultSetImpl) .realClose (booleano) line: 7473 JDBC4ResultSet (ResultSetImpl) .close () line: 881 DelegatingResultSet.close () line: 152
    Linea DelegatingResultSet.close (): 152
    DelegatingPreparedStatement (DelegatingStatement) .close () line: 163
    (Questa è la mia class) linea Database.close (): 84

    Solo l’impostazione della dimensione del recupero non è l’approccio corretto. Il javadoc di Statement#setFetchSize() afferma già quanto segue:

    Fornisce al driver JDBC un suggerimento sul numero di righe che dovrebbero essere recuperate dal database

    Il driver è in realtà libero di applicare o ignorare il suggerimento. Alcuni driver lo ignorano, alcuni driver lo applicano direttamente, alcuni driver necessitano di più parametri. Il driver JDBC MySQL rientra nell’ultima categoria. Se si controlla la documentazione del driver JDBC MySQL, verranno visualizzate le seguenti informazioni (scorrere verso sinistra 2/3 fino all’intestazione ResultSet ):

    Per abilitare questa funzionalità, è necessario creare un’istanza di Statement nel modo seguente:

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

    Si prega di leggere l’ intera sezione del documento, descrive anche gli avvertimenti di questo approccio. Ecco un citare pertinente:

    Ci sono alcuni avvertimenti con questo approccio. Dovrai leggere tutte le righe nel set di risultati (o chiuderlo) prima di poter emettere qualsiasi altra query sulla connessione, altrimenti verrà generata un’eccezione.

    (…)

    Se l’istruzione si trova nell’ambito di una transazione, i blocchi vengono rilasciati al completamento della transazione (il che implica che l’istruzione deve essere completata per prima). Come con la maggior parte degli altri database, le istruzioni non sono complete finché tutti i risultati in sospeso sull’istruzione non vengono letti o il set di risultati attivo per l’istruzione viene chiuso.

    Se ciò non risolve il problema OutOfMemoryError (non Exception ), allora è probabile che tu stia memorizzando tutti i dati nella memoria di Java invece di elaborarli immediatamente non appena i dati arrivano. Ciò richiederebbe più cambiamenti nel tuo codice , forse una completa riscrittura. Ho risposto a domande simili prima qui .

    Non chiudere i ResultSet due volte.

    Apparentemente, quando si chiude Statement , tenta di chiudere il ResultSet corrispondente, come si può vedere in queste due righe dalla traccia dello stack:

    Linea DelegatingResultSet.close (): 152
    DelegatingPreparedStatement (DelegatingStatement) .close () line: 163

    Avevo pensato che il blocco fosse in ResultSet.close() ma in realtà era in Statement.close() che chiama ResultSet.close() . Dal momento che il ResultSet era già chiuso, è stato semplicemente bloccato.

    Abbiamo sostituito tutti i ResultSet.close() con results.getStatement().close() e rimosso tutti Statement.close() s, e il problema è ora risolto.

    Nel caso in cui qualcuno abbia lo stesso problema, l’ho risolto utilizzando la clausola LIMIT nella mia query.

    Questo problema è stato segnalato a MySql come un bug (lo trovi qui http://bugs.mysql.com/bug.php?id=42929 ) che ora ha lo stato di “non un bug”. La parte più pertinente è:

    Al momento non è ansible chiudere un set di risultati “midstream”

    Poiché devi leggere TUTTE le righe, dovrai limitare i risultati della query utilizzando una clausola come WHERE o LIMIT. In alternativa, prova quanto segue:

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

    Potrebbe non essere l’ideale, ma almeno ti allontana da vicino.

    Se si utilizza jdbc di spring, è necessario utilizzare un creatore di preparazione della preparazione insieme a SimpleJdbcTemplate per impostare fetchSize come intero.MIN_VALUE. È descritto qui http://neopatel.blogspot.com/2012/02/mysql-jdbc-driver-and-streaming-large.html

    Si blocca perché anche se si interrompe l’ascolto, la richiesta continua comunque. Per chiudere ResultSet e Statement nell’ordine corretto, provare prima a chiamare statement.cancel ():

     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; } } } 

    Resultstable scrollabile ignora fetchSize e recupera tutte le righe contemporaneamente causando l’errore di memoria.

    Per me ha funzionato correttamente durante l’impostazione useCursors = true, altrimenti The Scollable Resultset ignora tutte le implementazioni della dimensione di recupero, nel mio caso era 5000 ma Scrollable Resultset ha recuperato milioni di record contemporaneamente causando un uso eccessivo della memoria. il DB sottostante è MSSQLServer.

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