Je développe une application print qui utilise de grandes tables MySQL. Lors du chargement de grandes tables, je reçois une OutOfMemoryException
, car le pilote tente de charger la totalité de la table dans la mémoire de l'application.
J'ai essayé d'utiliser
statement.setFetchSize(Integer.MIN_VALUE);
mais alors chaque ResultSet que j'ouvre est suspendu à close()
; En regardant en ligne, j'ai trouvé que cela se produisait car il essayait de charger toutes les lignes non lues avant de fermer le ResultSet, mais ce n'est pas le cas puisque je le fais:
ResultSet existingRecords = getTableData(tablename);
try {
while (existingRecords.next()) {
// ...
}
} finally {
existingRecords.close(); // this line is hanging, and there was no exception in the try clause
}
Les blocages se produisent également pour les petites tables (3 lignes), et si je ne ferme pas le RecordSet (ce qui est arrivé dans une méthode), alors connection.close()
se bloque.
Trace de pile du blocage:
SocketInputStream.socketRead0 (FileDescriptor, octet [], int, int, int) ligne: non disponible [méthode native]
SocketInputStream.read (byte [], int, int) ligne: 129
ReadAheadInputStream.fill (int) line: 113
ReadAheadInputStream.readFromUnderlyingStreamIfNecessary (byte [], int, int) ligne: 160
ReadAheadInputStream.read (byte [], int, int) ligne: 188
MysqlIO.readFully (InputStream, octet [], int, int) ligne: 2428 MysqlIO.reuseAndReadPacket (Buffer, int) ligne: 2882
Ligne MysqlIO.reuseAndReadPacket (tampon): 2871
MysqlIO.checkErrorPacket (int) line: 3414
MysqlIO.checkErrorPacket () line: 910
MysqlIO.nextRow (champ [], int, booléen, int, booléen, booléen, booléen, tampon): 1405
RowDataDynamic.nextRecord (): 413
RowDataDynamic.next () line: 392 RowDataDynamic.close () ligne: 170
JDBC4ResultSet (ResultSetImpl) .realClose (boolean): 7473 JDBC4ResultSet (ResultSetImpl) .close () ligne: 881 DelegatingResultSet.close () ligne: 152
DelegatingResultSet.close () ligne: 152
DelegatingPreparedStatement (DelegatingStatement) .close () ligne: 163
(Ceci est ma classe) Database.close () line: 84
Ne fermez pas votre ResultSet
s deux fois.
Apparemment, lors de la fermeture d'une Statement
, il tente de fermer la ResultSet
correspondante, comme vous pouvez le voir sur ces deux lignes à partir de la trace de la pile:
DelegatingResultSet.close () ligne: 152
DelegatingPreparedStatement (DelegatingStatement) .close () ligne: 163
J'avais pensé que le blocage était dans ResultSet.close()
mais c'était en fait dans Statement.close()
qui appelle ResultSet.close()
. Puisque la ResultSet
était déjà fermée, elle est restée bloquée.
Nous avons remplacé toutes les ResultSet.close()
par results.getStatement().close()
et supprimé toutes les Statement.close()
s. Le problème est maintenant résolu.
Définir uniquement la taille d'extraction n'est pas la bonne approche. Le javadoc de Statement#setFetchSize()
indique déjà ce qui suit:
Donne au pilote JDBC un indice quant au nombre de lignes à extraire de la base de données
Le pilote est en fait libre d'appliquer ou d'ignorer l'indicateur. Certains pilotes l'ignorent, d'autres l'appliquent directement, d'autres nécessitent davantage de paramètres. Le pilote JDBC MySQL entre dans la dernière catégorie. Si vous consultez la Documentation du pilote MySQL JDBC , vous verrez les informations suivantes (faites défiler les 2/3 environ jusqu'à l'en-tête ResultSet):
Pour activer cette fonctionnalité, vous devez créer une instance d'instruction de la manière suivante:
stmt = conn.createStatement(Java.sql.ResultSet.TYPE_FORWARD_ONLY, Java.sql.ResultSet.CONCUR_READ_ONLY); stmt.setFetchSize(Integer.MIN_VALUE);
Veuillez lire la section entière du document, elle décrit également les réserves de cette approche. Voici une citation pertinente:
Il y a quelques mises en garde avec cette approche. Vous devrez lire toutes les lignes du jeu de résultats (ou le fermer) avant de pouvoir émettre d'autres requêtes sur la connexion, sinon une exception sera levée.
(...)
Si l'instruction est dans la portée d'une transaction, des verrous sont libérés à la fin de la transaction (ce qui implique que l'instruction doit être terminée en premier). Comme avec la plupart des autres bases de données, les instructions ne sont pas complètes tant que tous les résultats en attente sur l'instruction ne sont pas lus ou que l'ensemble de résultats actifs de l'instruction n'est pas fermé.
Si cela ne résout pas la OutOfMemoryError
(pas Exception
), il est probable que vous stockiez toutes les données dans la mémoire de Java au lieu de les traiter immédiatement dès que les données arrivent. changements dans votre code, peut-être une réécriture complète. J'ai déjà répondu à une question similaire ici .
Au cas où quelqu'un aurait le même problème, je l'ai résolu en utilisant la clause LIMIT dans ma requête.
Ce problème a été signalé à MySql comme un bogue (le trouver ici http://bugs.mysql.com/bug.php?id=42929 ) qui a maintenant le statut "pas un bogue". La partie la plus pertinente est:
Il n’existe actuellement aucun moyen de fermer un jeu de résultats "à mi-parcours"
Comme vous devez lire TOUTES les lignes, vous devrez limiter les résultats de votre requête à l'aide d'une clause telle que WHERE ou LIMIT. Sinon, essayez ce qui suit:
ResultSet rs = ...
while(rs.next()) {
...
if(bailOut == true) { break; }
}
while(rs.next()); // This will deplete the remaining rows on the stream
rs.close();
Ce n'est peut-être pas idéal, mais au moins, cela vous permet de vous en sortir.
Si vous utilisez spring jdbc, vous devez utiliser un créateur d'instructions préparées avec SimpleJdbcTemplate pour définir fetchSize comme Integer.MIN_VALUE. Son décrit ici http://neopatel.blogspot.com/2012/02/mysql-jdbc-driver-and-streaming-large.html
Scrollable Resultset ignore fetchSize et extrait toutes les lignes en même temps, provoquant une erreur excessive.
Pour moi, cela fonctionnait correctement en définissant useCursors = true; sinon, le jeu de résultats défilable ignore toutes les implémentations de la taille d'extraction; dans mon cas, il s'agissait de 5 000, mais Scrollable Resultset a extrait des millions d'enregistrements à la fois, entraînant une utilisation excessive de la mémoire. La base de données sous-jacente est MSSQLServer.
jdbc: jtds: sqlserver: // localhost: 1433/ACS; TDS = 8.0; useCursors = true
Il se bloque parce que même si vous arrêtez d'écouter, la demande continue . Pour fermer le ResultSet et l'instruction dans le bon ordre, essayez d'abord d'appeler 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;
}
}
}