Потоковая обработка больших наборов результатов с помощью MySQL

Я разрабатываю приложение spring, которое использует большие таблицы 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) строка: недоступна [native method]
SocketInputStream.read(byte [], int, int): 129
ReadAheadInputStream.fill(int): 113
ReadAheadInputStream.readFromUnderlyingStreamIfNecessary(byte [], int, int): 160
ReadAheadInputStream.read(byte [], int, int) строка: 188
MysqlIO.readFully(InputStream, byte [], int, int): 2428 MysqlIO.reuseAndReadPacket(Buffer, int): 2882
MysqlIO.reuseAndReadPacket(буфер): 2871
MysqlIO.checkErrorPacket(int): 3414
MysqlIO.checkErrorPacket(): 910
MysqlIO.nextRow(поле [], 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

Ответ 1

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

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

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

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

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

Ответ 2

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

Дает драйвер JDBC подсказка относительно количества строк, которые должны быть извлечены из базы данных

Драйвер фактически может применять или игнорировать подсказку. Некоторые драйверы игнорируют его, некоторые драйверы применяют его напрямую, некоторым драйверам требуется больше параметров. Драйвер JDBC MySQL входит в последнюю категорию. Если вы проверите документацию по MySQL JDBC, вы увидите следующую информацию (прокрутите примерно 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 (not Exception), проблема в том, что вы сохраняете все данные в памяти Java вместо того, чтобы быстро его обрабатывать по мере поступления данных. Это потребует больше изменений в вашем коде, возможно, полного переписывания. Я ответил на аналогичный вопрос до здесь.

Ответ 3

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

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

В настоящий момент нет возможности закрыть результирующий набор "средний поток"

Поскольку вам нужно прочитать ВСЕ строки, вам придется ограничить результаты запроса, используя предложение типа 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();

Возможно, он не идеален, но, по крайней мере, он задерживает вас.

Ответ 5

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