Пул соединений: Hibernate + c3p0

Недавно загрузил пет-проект в сеть и понял, что Hibernate ну совсем не умеет обращаться с соединениями. Мой лимит на десять штук быстро исчерпывался, и приложение приходилось перезапускать, чтобы снова заработало.

Message: The internal connection pool has reached its maximum size and no connection is currently available!  
Description: The server encountered an unexpected condition that prevented it from fulfilling the request.

Exception:
org.hibernate.HibernateException: The internal connection pool has reached its maximum size and no connection is currently available!
    at org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl$PooledConnections.pollConnection(DriverManagerConnectionProviderImpl.java:423)
    at org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl.getConnection(DriverManagerConnectionProviderImpl.java:204)
    at org.hibernate.internal.NonContextualJdbcConnectionAccess.obtainConnection(NonContextualJdbcConnectionAccess.java:38)
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.acquireConnectionIfNeeded(LogicalConnectionManagedImpl.java:106)
    at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getPhysicalConnection(LogicalConnectionManagedImpl.java:136)

Получал такое исключение, и сначала проверил все сессии и соединения. Обнаружил, что всё закрываю, и потом понял, что не использую пул соединений. Я не тупой, просто пул мне до этого в проекте не был нужен (я с ним долгое время работал локально и был единственным юзером). А теперь нужно бы его добавить.

Что такое connection pool и зачем он нужен

Про пулы соединений я уже писал пару лет назад, когда делал проект на Спринге и подключал туда Hikari. Но не случится ничего плохого, если я немного повторюсь.

Connection pool / пул соединений – по сути, кэширование соединений с базой данных, чтобы использовать их заново. При каждом обращении к базе приложение должно установить соединение с ней, а эта операция довольно дорогостоящая и медленная. Использование пула помогает уменьшить нагрузку, увеличить скорость и сэкономить ресурсы (и отделаться от назойливых ошибок вроде той в начале статьи, или даже переполнения памяти в другом моём проекте).

Теперь, после закрытия соединения (connection.close()) оно переходит в пул, где лежит заданное время. И если какому-то методу снова понадобится обратиться к базе, ему не нужно будет устанавливать его заново – он просто получит уже установленное соединение. Если же какое-то время никто не хочет использовать соединения в пуле повторно, то они наконец-то закрываются с концами.

Ниже покажу, как задавать максимальное число соединений в пуле и настраивать частоту очистки и время жизни неиспользуемых соединений.

Но сначала немного про выбранный мной инструмент (c3p0).

c3p0 vs Hikari

В гитхабе создатель описывает своё творение как высокопроизводительную библиотеку пула соединений JDBC с поддержкой кэширования и повторного использования PreparedStatements. Это одна из самых популярных опций, и одно из главных преимуществ – удобная интеграция с Hibernate и гибкая настройка.

У Hikari, в сравнении с c3p0, выше надёжность, чаще обновления и лучше поддержка Spring. Оно и понятно, сам проект крупнее. Но я решил попробовать c3p0 в этом проекте.

Добавление в проект и конфигурация c3p0

Для подключения библиотеки добавляем зависимость в pom.xml:

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-c3p0</artifactId>
            <version>5.3.6.Final</version>
        </dependency>

Этого должно хватить для версий hibernate новее пятой. Иначе можно добавить ещё:

        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.5</version>
        </dependency>

Конфигурация в xml-файле

Теперь можно настроить пул. Так как у меня в проекте уже есть xml файл с настройками подключения к базе, туда я и буду добавлять новые строчки.

<hibernate-configuration>
    <session-factory>
        <property name="connection.driver_class">net.sourceforge.jtds.jdbc.Driver</property>
        <property name="connection.url">*********</property>
        <property name="connection.username">*******</property>
        <property name="connection.password">*******</property>
        <property name="dialect">org.hibernate.dialect.SQLServerDialect</property>

        <property name="hibernate.c3p0.max_size">1000</property>
        <property name="hibernate.c3p0.timeout">7200</property>
        <property name="hibernate.c3p0.max_statements">150</property>
        <property name="hibernate.c3p0.idle_test_period">600</property>

        <mapping class="app.models.User" />
        <...>
    </session-factory>
</hibernate-configuration>

После логина, пароля и адреса базы и перед всеми маппингами я добавил четыре строчки.

hibernate.c3p0.max_size – максимальное количество соединений

hibernate.c3p0.timeout – время (в секундах), после которого неиспользуемое соединение будет закрыто

hibernate.c3p0.max_statements – сколько prepared statementов кэшировать

hibernate.c3p0.idle_test_period – интервал (в секундах) для проверки неактивных соединений в пуле

idle_test_period должен быть меньше timeout для корректной работы. Каждые idle_test_period секунд все соединения в пуле будут проверяться. Если они не используются (idle) и пролежали больше timeout, то будут закрыты.

Среди других настроек, которые можно прописать в xml:

hibernate.c3p0.min_size – минимальное количество соединений (которые всегда будут в пуле)

hibernate.c3p0.acquire_increment – сколько новых соединений добавлять при нехватке (по умолчанию добавляется три)

hibernate.c3p0.validate – проверять ли соединение перед использованием

hibernate.c3p0.maxIdleTimeExcessConnections – указывает время в секундах, через которое удаляются все idle соединения, пока не будет достигнуто количество min_size. Показатель должен быть меньше timeout, чтобы иметь эффект.

hibernate.c3p0.unreturnedConnectionTimeout – указывает время в секундах, через которое зависшие соединения будут бесцеремонно уничтожатся. Крайняя мера для приложений с утечкой данных, чтобы не переполнить пул.

hibernate.c3p0.checkoutTimeout – указывает время в секундах, которое можно ожидать освободившегося подключения, если в пуле не осталось свободных.

С такими настройками в xml-файле ничего в джава-классах менять не нужно.

public class HibernateUtilSQLMain {
    private static SessionFactory sessionFactory = buildSessionFactory();

    private static SessionFactory buildSessionFactory() {
        try {
            return new Configuration().configure("sqlserverMain.cfg.xml").buildSessionFactory();
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }

}

Конфигурация в java-классе HibernateUtil

Если же вы устанавливаете соединение с базой в классе HibernateUtil, а не используете файл с настройками, то работы предстоит немного больше. Нужно создать объект типа ComboPooledDataSource, и туда уже впихивать все настройки соединения и пула.

public class HibernateUtil {
    private static SessionFactory sessionFactory;

    public static SessionFactory buildSessionFactory() {
        try {
            ComboPooledDataSource dataSource = new ComboPooledDataSource();
            dataSource.setDriverClass("net.sourceforge.jtds.jdbc.Driver"); 
            dataSource.setJdbcUrl("***************");
            dataSource.setUser("*********");
            dataSource.setPassword("*********");

            dataSource.setMinPoolSize(5);
            dataSource.setMaxPoolSize(1000);             
            dataSource.setAcquireIncrement(5);
            dataSource.setMaxStatements(150);
            dataSource.setIdleConnectionTestPeriod(600); 
            dataSource.setMaxIdleTime(7200);             

            Properties hibernateProps = new Properties();
            hibernateProps.put("hibernate.dialect", "org.hibernate.dialect.SQLServerDialect");
            hibernateProps.put("hibernate.hbm2ddl.auto", "validate");
            hibernateProps.put("hibernate.show_sql", "true");

            Configuration configuration = new Configuration();
            configuration.setProperties(hibernateProps);

            //добавляем Entity-классы
            configuration.addAnnotatedClass(*******************);

            ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
                    .applySettings(configuration.getProperties())
                    .applySetting("hibernate.connection.datasource", dataSource)
                    .build();

            sessionFactory = configuration.buildSessionFactory(serviceRegistry);
            return sessionFactory;

        } catch (PropertyVetoException e) {
            throw new RuntimeException("Ошибка при инициализации c3p0", e);
        }
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory != null ? sessionFactory : buildSessionFactory();
    }
}

В таблице ниже сопоставил методы из ComboPooledDataSource и настройки из файла xml.

c3p0.acquireIncrement	        hibernate.c3p0.acquire_increment
c3p0.idleConnectionTestPeriod	hibernate.c3p0.idle_test_period
c3p0.initialPoolSize	        нет аналога
c3p0.maxIdleTime	        hibernate.c3p0.timeout
c3p0.maxPoolSize	        hibernate.c3p0.max_size
c3p0.maxStatements	        hibernate.c3p0.max_statements
c3p0.minPoolSize	        hibernate.c3p0.min_size
c3p0.testConnectionOnCheckout 	hibernate.c3p0.validate

Подробнее об этом проекте в документации

Проверка работы пула

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

26-Jun-2025 22:14:44.223 INFO [http-nio-8080-exec-1] com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource. Initializing c3p0 pool... com.mchange.v2.c3p0.PoolBackedDataSource@d379ba64 [ connectionPoolDataSource -> com.mchange.v2.c3p0.WrapperConnectionPoolDataSource@418f3ab8 [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, maxConnectionAge -> 0, maxIdleTime -> 120, maxIdleTimeExcessConnections -> 60, maxPoolSize -> 1000, maxStatements -> 150, maxStatementsPerConnection -> 0, minPoolSize -> 5, <.....> ]

Далее в базе можно отследить, сколько на самом деле соединений используются.

SELECT COUNT(*) AS ActiveConnections
FROM sys.dm_exec_sessions
WHERE is_user_process = 1;

В следующей разбивки все джава-соединения будут в строке с jTDS:

SELECT program_name, COUNT(*) AS cnt
FROM sys.dm_exec_sessions
WHERE is_user_process = 1
GROUP BY program_name
ORDER BY cnt DESC;

У меня пул отказывался очищать соединения даже после таймаута, поэтому я ещё выполнил следующий запрос:


SELECT
    s.session_id,
    s.login_name,
    s.host_name,
    s.program_name,
    s.status,
    s.cpu_time,
    s.memory_usage,
    s.reads,
    s.writes,
    s.last_request_end_time,
    DATEDIFF(MINUTE, s.last_request_end_time, GETDATE()) AS minutes_idle
FROM sys.dm_exec_sessions s
LEFT JOIN sys.dm_exec_requests r ON s.session_id = r.session_id
WHERE
    s.is_user_process = 1
    AND r.session_id IS NULL       
    AND s.status = 'sleeping'
    AND s.program_name LIKE '%jTDS%'
    AND DATEDIFF(MINUTE, s.last_request_end_time, GETDATE()) > 5
ORDER BY minutes_idle DESC;

Он показывает «спящие» соединения, которые не использовались дольше пяти минут (для теста я задал таймаут в две минуты). Таким образом, мы увидим, если пул работает неверно и не удаляет соединения, когда должен был бы.

Следующий анализ можно провести, добавив вывод stacktrace в xml-файле:

        <property name="hibernate.c3p0.debugUnreturnedConnectionStackTraces">true</property>

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

Оставьте комментарий