Причины утечки пула соединений

У меня были (я думаю) проблемы с пулом соединений. В частности, в моих журналах отображается сообщение:

org.apache.tomcat.dbcp.pool2.impl.DefaultPooledObject$AbandonedObjectCreatedException: объект пула, созданный [время] с помощью следующего кода, не был возвращен в пул

Я проверил методы, перечисленные в трассировке стека, которые показывают журналы, но не смог найти виновника (я всегда закрываю ResultSet, PreparedStatement и Connection в конце каждого метода).

У меня есть метод, который выполняет два запроса, возможно, я не выполняю его должным образом.

Его макет выглядит следующим образом:

ConnectionPool pool = ConnectionPool.getInstance();
Connection connection = pool.getConnection();
PreparedStatement ps = null;
PreparedStatement rowsPs = null;
ResultSet rs = null;
ResultSet rowsRs = null;

String query = "SELECT SQL_CALC_FOUND_ROWS ...";
String totalRowsQuery = "SELECT FOUND_ROWS() AS RowCount";

try {
    ps = connection.prepareStatement(query);
    [set ps params]
    rs = ps.executeQuery();
    [process rs]

    rowsPs = connection.prepareStatement(totalRowsQuery);
    rowsRs = rowsPs.executeQuery();
    [process rowsRs]
} catch (SQLException e) {
    [handle e]
} finally {
    DBUtil.closeResultSet(rs);
    [close rowsRs]
    [close ps]
    [close rowsPs]
    [close connection]
}

Где пример метода DBUtils:

public static void closeResultSet(ResultSet rs)
{
    try
    {
        if (rs != null)
            rs.close();
    }
    catch (SQLException sqle)
    {
        sqle.printStackTrace();
    }
}

Общий вид этого метода кажется приемлемым? Должен ли я обрабатывать соединение по-другому? Или это какой-то другой метод, который вызывает регистрацию ошибок?

Спасибо.

Дополнительная информация

Я также получаю SQLException:

java.sql.SQLException: соединение com.mysql.jdbc.JDBC4Connection@[некоторое число] закрыто

в строке: rowsPs = connection.prepareStatement(totalRowsQuery);

Это означает, что где-то раньше соединение было закрыто. Я нигде явно не закрываю соединение. Возможно ли, что какой-то другой вызываемый метод доступа к данным каким-то образом закрывает соединение в этом методе? (pool.getConnection() звонит dataSource.getConnection())

Обновление: я попытался использовать попытку с ресурсами, как было предложено, но проблема не устранена.

Класс ConnectionPool, на который есть ссылка в первом фрагменте кода выше:

public class ConnectionPool 
{

    private static ConnectionPool pool = null;
    private static DataSource dataSource = null;

    public synchronized static ConnectionPool getInstance()
    {

        if ( pool == null ) {
            pool = new ConnectionPool();
        }
            return pool;
    }

    private ConnectionPool()
    {
        try {
            InitialContext ic = new InitialContext();
            dataSource = (DataSource) 
                    ic.lookup([jdbc/dbName]);
        }

        catch (Exception e) {
            e.printStackTrace();
        } 
    }

    public Connection getConnection()
    {
        try {
            return dataSource.getConnection();
        }
        catch (SQLException sqle) {
            sqle.printStackTrace();
            return null;
        }
    }
    public void freeConnection(Connection c)
    { 
        try {
            c.close();
        }
        catch (SQLException sqle) {
            sqle.printStackTrace();
        }
    }
}

Дополнительный источник: элемент ресурса My Pool:

<Resource auth="Container" driverClassName="com.mysql.jdbc.Driver"  
            logAbandoned="true" maxActive="100" maxIdle="30" maxWait="10000" 
            removeAbandonedOnBorrow="true" 
            removeAbandonedTimeout="60" type="javax.sql.DataSource" 
            testWhileIdle="true" testOnBorrow="true" 
            validationQuery="SELECT 1 AS dbcp_connection_test"/>

Обновление: я включил журнал медленных запросов, но, несмотря на то, что снова выдается Exception, журнал медленных запросов ничего не регистрирует (ни один запрос не занимает более 10 секунд).

Таким образом, похоже, дело не в том, что запрос занимает более 60 секунд.

Все еще неясно, что вызывает это.


person theyuv    schedule 06.10.2016    source источник
comment
Что делать, если строка в finally не работает? Никакие нисходящие линии выполняться не будут. Я бы просто закрыл соединение, потому что все разумные реализации JDBC будут каскадно закрывать все связанные с ним ресурсы.   -  person Marko Topolnik    schedule 06.10.2016
comment
Хорошо спасибо. Но на самом деле это не доходит до сути проблемы. Видите ли вы какую-либо причину, по которой строки в этом предложении finally не будут работать?   -  person theyuv    schedule 06.10.2016
comment
Да, каждая строка может вызывать исключение. Такая причина.   -  person Marko Topolnik    schedule 06.10.2016
comment
Я бы увидел эти исключения в своем журнале, нет? Я печатаю их. Все ресурсы закрываются с помощью класса util, который окружает оператор в предложении try и печатает трассировку стека, если возникает исключение.   -  person theyuv    schedule 06.10.2016
comment
Соединение устанавливается с помощью pool.getConnection() и освобождается с помощью connection.close(), это инвариант пула. Так что вам лучше найти причину, по которой строка connection.close() либо не достигается, либо завершается ошибкой. Или, как еще более экзотическое объяснение, переменная connection переназначается и исходное соединение разрывается.   -  person Marko Topolnik    schedule 06.10.2016
comment
хм, безопасно ли получить несколько подключений от DataSource? Например, в рамках метода, подобного приведенному выше, я вызываю несколько других методов доступа к базе данных, которые имеют одинаковую общую структуру (каждый получает соединение, а затем закрывает его). Есть ли ограничение на количество подключений, которые я могу получить, пока другие еще открыты?   -  person theyuv    schedule 06.10.2016
comment
Используйте попытку с ресурсами, и большинство ваших проблем, вероятно, исчезнет.   -  person Mark Rotteveel    schedule 06.10.2016
comment
@MarkRotteveel Я бы поместил свои rs и ps в попытку с ресурсами, но я не понимаю, как это что-то изменит. На данный момент эти ресурсы закрыты в блоке finally и обернуты операторами try-catch.   -  person theyuv    schedule 07.10.2016
comment
Если закрытие любого из них завершается неудачей, то закрытие соединения никогда не происходит; использование try-with-resources защищает вас от этого, не требуя большого количества шаблонов.   -  person Mark Rotteveel    schedule 07.10.2016
comment
Спасибо. Я что-то упустил о том, как работают исключения? вызовы в моем фрагменте кода выше, такие как close rs, выполняются в методах util, которые окружают rs.close() в блоке try and catch (я обновил вопрос с образцом этого кода). Не будет ли это означать, что даже если rs.close() завершится ошибкой, код просто поднимется со следующей строки в блоке finally (т. е. закроет rowRs), а не выйдет из всего метода?   -  person theyuv    schedule 07.10.2016
comment
Это может быть очень медленный запрос. Этот журнал в основном говорит, что соединение использовалось кем-то слишком долго. Пул не может знать, утечка ли это или клиент просто работает медленно.   -  person xiaofeng.li    schedule 26.10.2016
comment
Почему скорость клиента влияет на запрос? Запрос происходит на моем сервере.   -  person theyuv    schedule 26.10.2016
comment
@NicolasFilotto Это одноэлементный класс для получения соединений из пула соединений. Это очень просто. Я могу опубликовать код, дайте мне знать, если вам это нужно.   -  person theyuv    schedule 26.10.2016
comment
как долго ваши запросы?   -  person Nicolas Filotto    schedule 26.10.2016
comment
Я подозреваю, что ваше соединение уже прервано, прежде чем вы попытаетесь запустить второй запрос, предполагая, что первый запрос длится более 60 с, попробуйте увеличить removeAbandonedTimeout до 300   -  person Nicolas Filotto    schedule 26.10.2016
comment
Запрос, который появлялся в трассировках стека этой проблемы, действительно мой самый длинный (с точки зрения строк). Приблизительно 60 строк (я предполагаю, что такие длинные запросы - плохая практика). Однако, когда я запускаю его, выполнение не занимает почти 60 секунд (скорее 1-2).   -  person theyuv    schedule 26.10.2016
comment
Возможно, вы что-то понимаете в длине запроса (потому что, как я уже сказал, запрос у меня самый длинный); но может ли запрос, который обычно занимает 1-2 секунды, занять более 60 секунд?   -  person theyuv    schedule 26.10.2016
comment
Оба сообщения об ошибках указывают на то, что вы ссылаетесь на одно и то же соединение в нескольких местах. Вы можете доказать это с помощью журналов базы данных. Вы пытались обеспечить эксклюзивность подключения? (посмотрев на ваш код, вы этого не сделали). Контракт выглядит следующим образом: каждое заимствованное соединение необходимо вернуть в пул, вы получаете ошибки, потому что два оператора заимствовали одну и ту же ссылку, и вы можете закрыть (вернуть) ее только один раз. Возможно ли, что у вас есть состояние гонки pool.getConnection()? Попробуйте и синхронизировать его.   -  person Palcente    schedule 31.10.2016
comment
@Palcente Это звучит логично, позвольте мне проверить эту проблему, и я сообщу вам. Я не проверял журналы БД, но журналы доступа могут подтвердить то, что вы описываете. Спасибо.   -  person theyuv    schedule 31.10.2016
comment
@Palcente разве пул соединений не обеспечивает синхронизацию?   -  person theyuv    schedule 31.10.2016
comment
Действительно ли Connection connection является локальной переменной метода? Можешь сделать final на всякий случай?   -  person Roman    schedule 02.11.2016


Ответы (4)


Общие практические правила при работе с соединениями JDBC в пуле:

  1. Не держите более одного ResultSet открытым одновременно для каждого соединения. Закройте первый ResultSet и связанный с ним оператор перед открытием второго.

  2. Всегда закрывайте ресурсы точно в обратном порядке: create(s1)->execute(r1)->close(r1)->close(s1)->create(s2)->execute(r2)-> закрыть (r2) -> закрыть (s2)

  3. Убедитесь, что длительная обработка запросов не превышает максимальное время жизни соединения, разрешенное пулом.

  4. Даже если максимальное время жизни соединения не превышено, соединение может быть потеряно из-за небольших сбоев в сетевом соединении.

person Serg M Ten    schedule 26.10.2016
comment
1 и 2 делаю. По поводу №3: ни один из моих запросов не занимает больше 1-2 секунд. Могут ли они когда-либо занимать значительно больше времени, чем когда я их запускаю (они основаны на производительности моего сервера, а не клиента, верно)? мне кажется, что это не просто какое-то случайное прерывание. - person theyuv; 26.10.2016

Эти 2 строки занимают более 60 секунд, поэтому пул соединений решает, что ваше соединение прервано, и закрывает его. Позже ваш код пытается использовать соединение, но оно уже отключено пулом соединений.

rs = ps.executeQuery();
[process rs]

Поскольку вы уверены, что выполнение Query() не займет много времени, сделайте отладочную печать с идентификатором соединения/потока/запроса и временем для части [process rs]. Если вы видите что-то вроде 50+ секунд для этой части, вам следует оптимизировать [process rs] или считать данные и спрятать их в памяти перед обработкой.

person Alexander Anikin    schedule 31.10.2016

Превышает ли сервер mysql тайм-аут соединений? Это также может быть связано с ядром, если соединение долгое время простаивает, базовое соединение tcp может быть прервано.

person edlerd    schedule 29.10.2016

Попробуйте уменьшить значение removeAbandonedTimeout примерно до 20 или 15. Ваш maxWait составляет 10 секунд, но если соединения прерываются, вы ждете 60 секунд, прежде чем возвращать их.

Обратите внимание, что это не решение, а просто проверка, действительно ли ваши соединения зависают.

person Abinash    schedule 31.10.2016