Используем пул соединений к Базе Данных: Hikari Connection Pool + JDBC

Продолжаю делиться полезной информацией, которая помогала мне в выполнении недавнего тестового задания: без использования фреймворков (вроде Spring) нужно было организовать работу с базой данных сразу у нескольких потоков.
В прошлой статье я описал, как подключаться к базе данных (на примере H2) и как потом с ней работать (писать запросы с параметрами и без), а теперь пришло время разобраться с ситуацией, когда одного подключения на всю программу может не хватить.
В таком случае на помощь приходят пулы (pool) – компоненты, которые хранят набор ресурсов. Клиент пула эти ресурсы может «брать в аренду» и использовать. Главное их потом вернуть. А так как в этой статье речь про подключения к базе данных (объекты типа Connection), то и пулы нужны соответствующие: Connection Pool.
Плюсы пулов в том, что они позволяют ограничивать использование ресурсов, а значит, экономят память, а также время на создание и установку подключений. Ну и они упрощают жизнь разработчику: вам не нужно самим беспокоиться о том, как же правильно организовать раздачу ресурсов, чтобы неожиданно не получилось, как с сорокой-белобокой («этому дала и этому дала а этому не дала»).
Добавление зависимости HikariCP в pom.xml
Конечно, есть разные пулы, но я остановился на HikariCP. В моём проекте был не особо принципиален выбор, и я просто выбрал один из самых популярных вариантов. Итак, чтобы добавить выбранный пул, я прописал в pom.xml зависимость:
com.zaxxer HikariCP 4.0.3
И если вы хотите увидеть логи Hikari, то добавьте SLF4J Simple Binding: выводит сообщения в System.err – уровень Info или выше.
org.slf4j slf4j-simple 2.0.0 test
Настройка пула соединений
Пришло время заняться классом DBConnector, который в приложении отвечает за раздачу соединений. В прошлой статье метод getConnection() просто каждый раз создавал подключение к базе, но теперь поручим это объекту типа HikariDataSource:
private static HikariDataSource dataSource;
Вот такое поле я добавил в класс. И этот ресурс можно по-всякому настраивать перед использованием, за это отвечает класс HikariConfig:
private static HikariConfig config = new HikariConfig();
Поработал я с настройками в статическом блоке, но можно, конечно, и как-нибудь по-другому это организовать.
static < config.setJdbcUrl("jdbc:h2:~/test"); config.setUsername("sa"); config.setPassword(""); config.setConnectionTimeout(50000); config.setMaximumPoolSize(100); dataSource = new HikariDataSource(config); >
Основные настройки остаются всё теми же, что и в прошлой статье: это адрес базы (так как я юзаю h2 для простоты, то у меня и значение jdbc:h2:~/test), логин и пароль для подключения к базе (я использую стандартные данные). Но есть и более продвинутые настройки: мне по заданию нужно было обеспечить работу до ста потоков, поэтому и размер пула я обозначил в 100, таймаут на всякий случай увеличил до 50 секунд (что было пустой затеей)…
Настроек вообще хватает, на каждую из них есть по методу с соответствующим именем, но также есть метод addDataSourceProperty(), в который передаётся пара значений: строка с названием и значение. C этим методом настройка выглядела бы следующим образом:
config.addDataSourceProperty("connectionTimeout", 50000); config.addDataSourceProperty("maximumPoolSize", 100);
А вот и список настроек:
- autoCommit
- connectionTimeout
- idleTimeout
- maxLifetime
- connectionTestQuery
- connectionInitSql
- validationTimeout
- maximumPoolSize
- poolName
- allowPoolSuspension
- readOnly
- transactionIsolation
- leakDetectionThreshold
Под конец мы инициализируем переменную dataSource – вызываем конструктор с параметром: new HikariDataSource(config).
Использование подключений с HikariCP
В методе getConnection больших перемен нет. Если раньше мы просили подключение у DriverManager, то теперь делаем это у dataSource. В остальном всё то же самое.
@SneakyThrows public Connection getConnection() throws SQLException
Соответственно, и использование объекта остаётся тем же – в клиенте ничего менять не нужно, так как объект connection возвращается абсолютно такой же, как и раньше. Взглянем на пример с методом, отвечающим за удаление записи из базы данных:
public void delete(Long id) < String sql = "DELETE FROM task WHERE task_id=?"; try (Connection connection = connector.getConnection(); PreparedStatement ps = connection.prepareStatement(sql)) < ps.setLong(1, id); ps.executeUpdate(); >catch (SQLException ex) < ex.printStackTrace(); >>
Как и положено, этот метод и его родительский класс даже не подозревают, что теперь соединение им подсунул какой-то Хикари.
Подробнее о работе с объектами в базе, о сервисах и создании REST приложения без Spring напишу в следующей статье. Созданный с пулом DBConnector там пригодится.
HikariCP (пул соединений) перестает выдавать соединения
Всем привет, столкнулся с проблемой в которой не могу разобраться. Подключил HikariCP, пользователей приложения около 50 человек, ~ через час пул соединений перестает выдавать соединения (HikariPool-1 — Connection is not available, request timed out after 3000ms.) Задавал разные настройки и тайминги, но проблема не исчезает, единственное что я придумал это создавать принудительно нативное соединение(но оно долго создается и интерфейс приложения подтормаживает). БД — PostgreSQL Вот мой код:
public enum MyConnectionPool < INSTANCE(); private HikariDataSource dataSource; MyConnectionPool() < if(dataSource==null) < HikariConfig config = new HikariConfig(); config.setDriverClassName(ConnectionProperty.DRIVER_NAME.getValue()); config.setJdbcUrl(ConnectionProperty.URL.getValue()); config.setUsername(ConnectionProperty.LOGIN.getValue()); config.setPassword(ConnectionProperty.PASS.getValue()); config.addDataSourceProperty("dataSource.cachePrepStmts","true"); config.addDataSourceProperty("dataSource.prepStmtCacheSize","250"); config.addDataSourceProperty("dataSource.prepStmtCacheSqlLimit","2048"); /*Перепробовал кучу параметров */ config.setMaximumPoolSize(80); config.setMinimumIdle(30); config.setIdleTimeout(0); config.setConnectionTimeout(3000);//3 секунды config.setMaxLifetime(0); config.setKeepaliveTime(60000);//10мин config.setLeakDetectionThreshold(2000); dataSource = new HikariDataSource(config); >> public Connection getConn() < if(dataSource!=null) < try < return dataSource.getConnection(); >catch (SQLException e) < try < Notification.show("Не получил соединение из пула, создал новое:"+e.getMessage()); >catch (Exception e1) < >//~ через час начинает срабатывать принудительное создание подключения и начинаются тормоза return CRMDatabaseConnection.getDatabaseConnection(); > >else < try < Notification.show("dataSource IS NULL, создал новое соединение"); >catch (Exception e1) < >return CRMDatabaseConnection.getDatabaseConnection(); //return null; > > public HikariDataSource getDataSource() < return dataSource; >>
Обращаюсь к базе вот таким способом:
try(Connection connection = MyConnectionPool.INSTANCE.getConn()) < String sql = "SELECT . " Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sql); while (rs.next()) < . >rs.close; stmt.close; >catch (Exception e)
Такое ощущение что пул создает соединения, после того как я использую соединение я его закрываю, соединение в пуле становится закрытым, и при повторном обращении к пулу он мне и выдает закрытое соединение. Хотя по логике connection (это HikariProxyConnection), и закрывая его я по cути говорю пулу, что соединение может использовать кто то другой, т.е физически оно не должно закрываться Делаю тесты:
int counter = 0; for(int i=0;i <1000;i++) < try (Connection connection = MyConnectionPool.INSTANCE.getConn()) < System.out.println(connection); String sql = "SELECT COUNT(1) AS COUNT from sys_leads LIMIT 100"; Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sql); while (rs.next())< int count = rs.getInt("COUNT"); System.out.println(count); counter++; >rs.close(); stmt.close(); > catch (Exception e) < System.out.println("ошибка:"+e); >System.out.println("итерация:"+counter); >
Пул возвращает одно и тоже соединение и как бы все нормально, ошибок нет. Если реализовать этот код в разных потоках то тоже все нормально, на каждый поток возвращается свое соединение и проблем нет
HikariCP — Database Connection Pool
The creation of new connections in the database requires some processing, especially when there is an authentication.
Therefore, to achieve higher performance is advisable to reuse the connections and not constantly open a new connection.
To solve this issue a connection pool is used, which makes it so that when a connection is dropped it will not actually be closed and be destroyed, so the connection will be put on hold to be reused when a new connection is needed.
This will offer great performance and also allows more parameter settings, including the best which is to limit the maximum number of connections to avoid overloading the database.
Hikari means Light in Japanese, is the most efficient and Java Database Connectivity (JDBC) is a Java API for accessing relational databases and is a very fast lightweight Java connection pool.
The official HikariCP repository can be found here on GitHub, check out the performance graphs and configuration information.
Netuno uses HikariCP to manage connections to PostgreSQL , MariaDB , Microsoft SQL Server and Oracle .
The configuration of HikariCP on Netuno is done next to the configuration of the database in the applications.
That is, within the application in the config folder in the environment configuration, such as:
- _development.json
- _production.json
In the «db» section, it contains the database settings.
Also for the default connection as for any other connection can be configure the HikariCP parameters in the same way.
So inside the default, where you have the host , port , name (database) , username and password , just add more HikariCP parameters as:
. "db": < "default": < "engine": "pg", "host": "localhost", "port": 5432, "name": "my_database", "username": "my_user", "password": "secret", "maximumPoolSize": 25, "minimumIdle": 1, "idleTimeout": 10000, "maxLifetime": 60000 > > .
For example, each of these extra HikariCP settings above are for:
- The maximumPoolSize sets the maximum size of connections in HikariCP, in this example the connection limit will be 25.
- The minimumIdle sets the minimum number of pending connections in the queue to be used.
- The idleTimeout is the time the connection can be queued.
- The maxLifetime is the maximum lifetime connection.
The HikariCP settings must be consistent with the database engine configuration. For example, the maximum number of connections and the time limits must never exceed what is defined in the database engine.
Any other HikariCP parameter can be configured in the same way, just add the desired configuration parameters.
See below the list of parameters that can be configured with HikariCP using Netuno. This configuration are either set as default by Netuno or are not applicable.
This property controls the default auto-commit behavior of connections returned from the pool. It is a boolean value. Default: true .
This property controls the maximum number of milliseconds that a Client/User will wait for a connection from the pool. If this time is exceeded without a connection becoming available, an SQLException will be thrown. The lowest acceptable connection timeout is 250 ms.
Default: 30000 (30 seconds)
This property controls the maximum amount of time that a connection is allowed to sit idle in the pool. This setting only applies when minimumIdle is defined to be less than maximumPoolSize . Idle connections will not be retired once the pool reaches minimumIdle connections. Whether a connection is retired as idle or not is subject to a maximum variation of +30 seconds, and average variation of +15 seconds. A connection will never be retired as idle before this timeout. A value of 0 means that idle connections are never removed from the pool. The minimum allowed value is 10000ms (10 seconds).
Default: 600000 (10 minutes)
This property controls the Maximum Lifetime of a connection in the pool. An in-use connection will never be retired, only when it is closed will it then be removed. On a connection-by-connection basis, minor negative attenuation is applied to avoid mass-extinction in the pool. Is strongly recommended setting this value, and it should be several seconds shorter than any database or infrastructure imposed connection time limit. A value of 0 indicates no maximum lifetime (infinite lifetime), subject of course to the idleTimeout setting. The minimum allowed value is 30000ms (30 seconds).
Default: 1800000 (30 minutes)
This property controls the minimum number of idle connections that HikariCP tries to maintain in the pool. If the idle connections dip below this value and total connections in the pool are less than maximumPoolSize , HikariCP will make the best effort to add additional connections quickly and efficiently. However, for higher performance and response, is recommend not set this value instead allow HikariCP to act as a fixed size connection pool.
Default: same as maximumPoolSize
This property controls the maximum size that the pool is allowed to have including both idle and in-use connections. Basically this value will define the maximum number of actual connections to the backend database. A reasonable value for this is best set by your performance environment. When the pool reaches this size and no idle connections are available, calls to getConnection() will block for up to connectionTimeout milliseconds before timing out.
Initialization Fail Timeout
This property controls whether the pool will «fail fast» if the pool cannot be seeded with an initial successfully connection. Any positive number is taken to be the number of milliseconds to attempt to acquire an initial connection; the application thread will be blocked during this period. If a connection cannot be acquired before this timeout occurs, an exception will be thrown. This timeout is applied after the connectionTimeout period. If the value is zero (0), HikariCP will attempt to obtain and validate a connection. If a connection is obtained but fails validation, an exception will be thrown and the pool will not be started. However, if a connection cannot be obtained, the pool will start but later efforts to obtain a connection may fail. A value less than zero will bypass any initial connection attempt, and the pool will start immediately while trying to obtain connections in the background. Consequently, later efforts to obtain a connection may fail.
This property sets the default catalog for databases that support the concept of catalogs. If this property is not specified, the default catalog defined by the JDBC driver is used.
Default: driver default
This property controls the maximum amount of time that a connection will be tested for aliveness. This value must be less than the connectionTimeout . Lowest acceptable validation timeout is 250 ms.
Leak Detection Threshold
This property controls the amount of time that a connection can be out of the pool before a message is logged indicating a possible connection leak. A value of 0 means leak detection is disabled. Lowest acceptable value for enabling leak detection is 2000 (2 seconds).
This property sets the default schema for databases that support the concept of schemas. If this property is not specified, the default schema defined by the JDBC driver is used.
Default: driver default
It is also possible to add settings to the DataSource, i.e. additional settings specific to the type of database being used.
To perform this more advanced configuration you will need to add the datasource configuration object inside the database connection configuration object.
. "db": < "default": < "engine": "mariadb", "host": "localhost", "port": 3306, "name": "my_database", "username": "my_user", "password": "secret", "maximumPoolSize": 25, "minimumIdle": 1, "idleTimeout": 10000, "maxLifetime": 60000, "datasource": < "cachePrepStmts": "true", "prepStmtCacheSize": 250, "prepStmtCacheSqlLimit": 2048 > > > .
Notice that the DataSource parameter settings are of type string, so Netuno always automatically converts to string anyway.
Using HikariCP it is possible to perform advanced settings to manage the load and behavior of connections into the database.
It can be optimized for the most various scenarios to guarantee its high performance.
Введение в HikariCP
В этом вводном руководстве мы узнаем о проекте пула соединений HikariCP JDBC . Это очень легкая (примерно 130 КБ) и молниеносная среда пула соединений JDBC, разработанная Бреттом Вулдриджем примерно в 2012 году.
2. Введение
Доступно несколько тестовых результатов для сравнения производительности HikariCP с другими платформами пула соединений, такими как c3p0 , dbcp2 , tomcat и vibur . Например, команда HikariCP опубликовала следующие бенчмарки (исходные результаты доступны здесь ):

Фреймворк такой быстрый, потому что были применены следующие методы:
- Разработка на уровне байт-кода — была выполнена некоторая экстремальная разработка на уровне байт-кода (включая собственное кодирование на уровне сборки).
- Микрооптимизации — хотя их едва можно измерить, в совокупности эти оптимизации повышают общую производительность.
- Интеллектуальное использование платформы коллекций — ArrayList был заменен настраиваемым классом FastList, который устраняет проверку диапазона и выполняет сканирование удаления от начала до конца.
3. Зависимость от Maven
Во-первых, давайте создадим пример приложения, чтобы выделить его использование. HikariCP поставляется с поддержкой всех основных версий JVM. Каждая версия требует своей зависимости. Для Java с 8 по 11 у нас есть:
dependency> groupId>com.zaxxergroupId> artifactId>HikariCPartifactId> version>3.4.5version> dependency>
HikariCP также поддерживает более старые версии JDK, вроде 6 и 7. Соответствующие версии можно найти здесь и здесь соответственно. Мы также можем проверить последние версии в Центральном репозитории Maven .
4. Использование
Теперь мы можем создать демонстрационное приложение. Обратите внимание, что нам нужно включить подходящую зависимость класса драйвера JDBC в pom.xml . Если зависимости не указаны, приложение выдаст исключение ClassNotFoundException .
4.1. Создание источника данных
Мы будем использовать HikariCP DataSource для создания одного экземпляра источника данных для нашего приложения:
public class DataSource private static HikariConfig config = new HikariConfig(); private static HikariDataSource ds; static config.setJdbcUrl( "jdbc_url" ); config.setUsername( "database_username" ); config.setPassword( "database_password" ); config.addDataSourceProperty( "cachePrepStmts" , "true" ); config.addDataSourceProperty( "prepStmtCacheSize" , "250" ); config.addDataSourceProperty( "prepStmtCacheSqlLimit" , "2048" ); ds = new HikariDataSource( config ); > private DataSource() > public static Connection getConnection() throws SQLException return ds.getConnection(); > >
Здесь следует отметить один момент — инициализацию в статическом блоке.
HikariConfig — это класс конфигурации, используемый для инициализации источника данных. Он поставляется с четырьмя известными обязательными параметрами: имя пользователя , пароль , jdbcUrl и dataSourceClassName .
Из jdbcUrl и dataSourceClassName мы обычно используем по одному. Однако при использовании этого свойства со старыми драйверами нам может потребоваться установить оба свойства.
В дополнение к этим свойствам есть несколько других доступных свойств, которые мы можем не найти в других платформах объединения:
- автофиксация
- время соединения вышло
- idleTimeout
- maxLifetime
- соединениеTestQuery
- соединениеInitSql
- валидациятаймаут
- максимальный размер пула
- имя_пула
- allowPoolSuspension
- только для чтения
- изоляция транзакции
- Порог обнаружения утечки
HikariCP выделяется из-за этих свойств базы данных. Он даже достаточно продвинут, чтобы самостоятельно обнаруживать утечки соединений.
Подробное описание вышеуказанных свойств можно найти здесь .
Мы также можем инициализировать HikariConfig с помощью файла свойств, расположенного в каталоге ресурсов :
private static HikariConfig config = new HikariConfig( "datasource.properties" );
Файл свойств должен выглядеть примерно так:
dataSourceClassName= //TBD dataSource.user= //TBD //other properties name should start with dataSource as shown above
Кроме того, мы можем использовать конфигурацию на основе java.util.Properties :
Properties props = new Properties(); props.setProperty( "dataSourceClassName" , //TBD ); props.setProperty( "dataSource.user" , //TBD ); //setter for other required properties private static HikariConfig config = new HikariConfig( props );
В качестве альтернативы мы можем инициализировать источник данных напрямую:
ds.setJdbcUrl( //TBD ); ds.setUsername( //TBD ); ds.setPassword( //TBD );
4.2. Использование источника данных
Теперь, когда мы определили источник данных, мы можем использовать его для получения соединения из настроенного пула соединений и выполнения действий, связанных с JDBC.
Предположим, у нас есть две таблицы с именами dept и emp для имитации варианта использования «сотрудник-отдел». Мы напишем класс для извлечения этих деталей из базы данных с помощью HikariCP.
Ниже мы перечислим операторы SQL, необходимые для создания примера данных:
create table dept( deptno numeric, dname varchar(14), loc varchar(13), constraint pk_dept primary key ( deptno ) ); create table emp( empno numeric, ename varchar(10), job varchar(9), mgr numeric, hiredate date, sal numeric, comm numeric, deptno numeric, constraint pk_emp primary key ( empno ), constraint fk_deptno foreign key ( deptno ) references dept ( deptno ) ); insert into dept values( 10, 'ACCOUNTING', 'NEW YORK' ); insert into dept values( 20, 'RESEARCH', 'DALLAS' ); insert into dept values( 30, 'SALES', 'CHICAGO' ); insert into dept values( 40, 'OPERATIONS', 'BOSTON' ); insert into emp values( 7839, 'KING', 'PRESIDENT', null, to_date( '17-11-1981' , 'dd-mm-yyyy' ), 7698, null, 10 ); insert into emp values( 7698, 'BLAKE', 'MANAGER', 7839, to_date( '1-5-1981' , 'dd-mm-yyyy' ), 7782, null, 20 ); insert into emp values( 7782, 'CLARK', 'MANAGER', 7839, to_date( '9-6-1981' , 'dd-mm-yyyy' ), 7566, null, 30 ); insert into emp values( 7566, 'JONES', 'MANAGER', 7839, to_date( '2-4-1981' , 'dd-mm-yyyy' ), 7839, null, 40 );
Обратите внимание: если мы используем базу данных в памяти, такую как H2, нам необходимо автоматически загрузить сценарий базы данных перед запуском фактического кода для извлечения данных. К счастью, H2 поставляется с параметром INIT , который может загружать скрипт базы данных из пути к классам во время выполнения. URL-адрес JDBC должен выглядеть так:
jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;INIT=runscript from 'classpath:/db.sql'
Нам нужно создать метод для извлечения этих данных из базы данных:
public static ListEmployee> fetchData() throws SQLException String SQL_QUERY = "select * from emp"; ListEmployee> employees = null; try (Connection con = DataSource.getConnection(); PreparedStatement pst = con.prepareStatement( SQL_QUERY ); ResultSet rs = pst.executeQuery();) employees = new ArrayList>(); Employee employee; while ( rs.next() ) employee = new Employee(); employee.setEmpNo( rs.getInt( "empno" ) ); employee.setEname( rs.getString( "ename" ) ); employee.setJob( rs.getString( "job" ) ); employee.setMgr( rs.getInt( "mgr" ) ); employee.setHiredate( rs.getDate( "hiredate" ) ); employee.setSal( rs.getInt( "sal" ) ); employee.setComm( rs.getInt( "comm" ) ); employee.setDeptno( rs.getInt( "deptno" ) ); employees.add( employee ); > > return employees; >
Затем нам нужно создать метод JUnit для его проверки. Поскольку мы знаем количество строк в таблице emp , мы можем ожидать, что размер возвращаемого списка должен быть равен количеству строк:
@Test public void givenConnection_thenFetchDbData() throws SQLException HikariCPDemo.fetchData(); assertEquals( 4, employees.size() ); >
5. Вывод
В этой краткой статье мы узнали о преимуществах использования HikariCP и его настройке.
Как всегда, полный исходный код доступен на GitHub .