Потоковые большие наборы результатов с MySQL

Я разрабатываю весеннее приложение, которое использует большие таблицы MySQL. При загрузке больших таблиц, я получаю OutOfMemoryException, поскольку драйвер пытается загрузить всю таблицу в память приложения.

Я пытался с помощью

statement.setFetchSize(Integer.MIN_VALUE);

но затем каждый ResultSet, который я открываю, зависает close(); Посмотрев в Интернете, я обнаружил, что это происходит потому, что он пытается загрузить непрочитанные строки перед закрытием ResultSet, но это не так, поскольку я делаю это:

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

Зависания случаются и для небольших таблиц (3 строки), и если я не закрою RecordSet (что произошло в одном методе), то connection.close() висит.


Стек следа зависания:

SocketInputStream.socketRead0 (FileDescriptor, byte [], int, int, int) строка: недоступно [собственный метод]
SocketInputStream.read (byte[], int, int) строка: 129
Строка ReadAheadInputStream.fill(int): 113
ReadAheadInputStream.readFromUnderlyingStreamIfNeeded (byte[], int, int) строка: 160
ReadAheadInputStream.read (byte[], int, int) строка: 188
MysqlIO.readFully (InputStream, byte [], int, int) строка: 2428 MysqlIO.reuseAndReadPacket(Buffer, int) строка: 2882
Строка MysqlIO.reuseAndReadPacket(Buffer): 2871
Строка MysqlIO.checkErrorPacket(int): 3414
MysqlIO.checkErrorPacket () строка: 910
Строка MysqlIO.nextRow(Field[], int, boolean, int, boolean, boolean, boolean, Buffer): 1405
RowDataDynamic.nextRecord () строка: 413
RowDataDynamic.next () строка: 392 RowDataDynamic.close() строка: 170
JDBC4ResultSet (ResultSetImpl).realClose (логическое значение) строка: 7473 JDBC4ResultSet(ResultSetImpl).close() строка: 881 DelegatingResultSet.close() строка: 152
DelegatingResultSet.close () строка: 152
DelegatingPreparedStatement (DelegatingStatement).close () строка: 163
(Это мой класс). Database.close() строка: 84

6 ответов

Решение

Не закрывайте ResultSet с дважды.

Видимо, при закрытии Statement он пытается закрыть соответствующий ResultSet, как вы можете видеть в этих двух строках из трассировки стека:

DelegatingResultSet.close () строка: 152
DelegatingPreparedStatement (DelegatingStatement).close () строка: 163

Я думал, что повешение было в ResultSet.close() но это было на самом деле в Statement.close() какие звонки ResultSet.close(), Так как ResultSet был уже закрыт, он просто висел.

Мы заменили все ResultSet.close() с results.getStatement().close() и удалил все Statement.close() с, и проблема теперь решена.

Только установка размера выборки не является правильным подходом. Javadoc ofStatement#setFetchSize() уже заявляет следующее:

Предоставляет драйверу JDBC подсказку о количестве строк, которые следует извлечь из базы данных.

Водитель может свободно применять или игнорировать подсказку. Некоторые драйверы игнорируют его, некоторые драйверы применяют его напрямую, некоторым драйверам нужно больше параметров. Драйвер MySQL JDBC попадает в последнюю категорию. Если вы посмотрите документацию драйвера JDBC для MySQL, вы увидите следующую информацию (прокрутите примерно на 2/3 до заголовка ResultSet):

Чтобы включить эту функцию, вам нужно создать экземпляр Statement следующим образом:

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

Пожалуйста, прочитайте весь раздел документа, в нем также описаны недостатки этого подхода. Вот соответствующий цитата:

Есть несколько предостережений при таком подходе. Вам нужно будет прочитать все строки в наборе результатов (или закрыть его), прежде чем вы сможете выполнить какие-либо другие запросы для соединения, иначе возникнет исключение.

(...)

Если оператор находится в области действия транзакции, блокировки снимаются по завершении транзакции (что означает, что сначала необходимо выполнить инструкцию). Как и в большинстве других баз данных, операторы не являются полными до тех пор, пока не будут прочитаны все результаты, ожидающие этого оператора, или пока не будет закрыт активный набор результатов для оператора.

Если это не исправит OutOfMemoryError (не Exception), тогда проблема, скорее всего, в том, что вы сохраняете все данные в памяти Java, а не обрабатываете их сразу же, как только они поступают. Это потребует дополнительных изменений в вашем коде, возможно, полного переписывания. Я ответил на аналогичный вопрос раньше здесь.

В случае, если у кого-то возникла такая же проблема, я решил ее с помощью предложения LIMIT в своем запросе.

Эта проблема была сообщена MySql как ошибка (найдите ее здесь http://bugs.mysql.com/bug.php?id=42929), которая теперь имеет статус "не ошибка". Самая важная часть это:

В настоящее время нет способа закрыть набор результатов "midstream"

Поскольку вам нужно прочитать ВСЕ строки, вам придется ограничить результаты запроса с помощью предложения типа WHERE или LIMIT. Или попробуйте следующее:

ResultSet rs = ...
while(rs.next()) {
   ...
   if(bailOut == true) { break; }
}

while(rs.next()); // This will deplete the remaining rows on the stream

rs.close();

Это не может быть идеальным, но, по крайней мере, это поможет вам преодолеть все трудности.

Если вы используете Spring JDBC, то вам нужно использовать создатель подготовленного состояния вместе с SimpleJdbcTemplate, чтобы установить fetchSize как Integer.MIN_VALUE. Это описано здесь http://neopatel.blogspot.com/2012/02/mysql-jdbc-driver-and-streaming-large.html

Он зависает, потому что, даже если вы перестанете слушать, запрос все еще продолжается. Чтобы закрыть ResultSet и Statement в правильном порядке, попробуйте сначала вызвать 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;
        }
    }
}

Scrollable Resultset игнорирует fetchSize и извлекает все строки сразу, вызывая ошибку из-за ошибки.

Для меня это работало правильно при установке useCursors = true, в противном случае Scrollable Resultset игнорирует все реализации размера выборки, в моем случае это было 5000, но Scrollable Resultset извлекал миллионы записей одновременно, вызывая чрезмерное использование памяти. лежащая в основе БД - MSSQLServer.

JDBC:jtds: SQLServer:// локальный:1433/ACS;TDS=8,0;useCursors= истина

Другие вопросы по тегам