При работе с JDBC (или какой-нибудь обёрткой над JDBC) процесс работы с базы строится одинаковым образом:
- Открываем соединение с базой
- Делаем запросы.
Это прекрасно работает и не вызывает никаких проблем ровно до той поры, пока все запросы выполняются в одном потоке. Но в современном мире приложения многопоточны. В самом деле, если у вас web приложение, то скорее всего в нём будет несколько нитей, которые обслуживают запросы. Если речь идёт о ETL приложении, то наверняка данные, над которыми оно работает, будут разделены на блоки, которые будут перерабатывать несколько параллельных потоков. Даже настольные приложения сейчас имеют несколько потоков. А соединение то у нас с базой одно. И это проблема.
Во-первых, JDBC это интерфейс и его реализация может быть потокобезопасной, а может и не быть. И если сейчас используется какая-либо база данных, которая позволяет разделять один единственный объект Connection между потоками, то в будущем может потребоваться перейти на другую базу, реализация JDBC драйвера которой не будет потокобезопасной. Во-вторых, запросы из разных потоков буду выполняться вразнобой, что может привести к интересным последствиям в БД. В-третьих, и главных, транзакция на одно соединение может быть только одна и если нам нужно несколько параллельных транзакций, то реализовать это не получится.
Какие есть варианты решения этой проблемы? Можно обвеситься мутексами и запрещать параллельный доступ к базе из разных нитей. Это действительно решит проблему, но возникает вопрос, зачем тогда многопоточность и где от неё выгода? Можно создавать соединение в каждой нити отдельно и по завершению нити закрывать его. Это тоже решит проблему, но плохо скажется на производительности приложения — открытие соединения с базой довольно дорогая процедура. И, наконец, лучшее решение — открыть некоторое количество соединений заранее (сформировать пул соединений), а потоки будут брать готовые соединения из пула по мере надобности и возвращать их обратно после использования.
Разумеется, лучше всего взять готовое решение, чем изобретать велосипед. Для примера я использую HikariCP и базу данных PostgreSQL.
Подготовка
Создадим пустой maven проект и добавим в него артефакты драйвера PostgreSQL JDBC и HikariCP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <postgresql.version>9.4-1206-jdbc42</postgresql.version> <hikaricp.version>2.4.3</hikaricp.version> </properties> <dependencies> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>${postgresql.version}</version> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>${hikaricp.version}</version> </dependency> </dependencies> |
Кроме того, надо подготовить базу в PostgreSQL:
1 2 3 4 5 6 | CREATE ROLE test WITH PASSWORD 'test'; ALTER ROLE test WITH LOGIN; CREATE DATABASE test OWNER test; GRANT CREATE ON DATABASE test TO test; |
Создание пула
1 2 3 4 | HikariConfig config = new HikariConfig(); config.setJdbcUrl( "jdbc:postgresql://192.168.75.6/test?user=test&password=test"); HikariDataSource ds = new HikariDataSource(config); |
Пул соединений создавать не сложнее, чем одно соединение напрямую через JDBC. Мы указываем jdbc URL в объекте конфигурации, в нём же задаём дополнительные параметры, если требуется, и создаём из него объект DataSource Из DataSource можно запросить объект Connection и работать с ним как обычно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /** * Query that create table. */ private static final String CREATE_QUERY = "CREATE TABLE EXAMPLE (GREETING VARCHAR(6), TARGET VARCHAR(6))"; /** * Query that populates table with data. */ private static final String DATA_QUERY = "INSERT INTO EXAMPLE VALUES('Hello','World')"; try (Connection db = ds.getConnection()) { try (Statement dataQuery = db.createStatement()) { dataQuery.execute(CREATE_QUERY); dataQuery.execute(DATA_QUERY); } } catch (SQLException ex) { System.out.println("Database connection failure: " + ex.getMessage()); return; } |
Использование пула
Но использовать пул в один поток неинтересно. Давайте попробуем сделать к базе несколько параллельных запросов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | Runnable readingThread = () -> { startLatch.countDown(); try { startLatch.await(); } catch (InterruptedException ex) { System.out.println("Synchronization failure: " + ex.getMessage()); return; } try (Connection db = ds.getConnection()) { try (PreparedStatement query = db.prepareStatement("SELECT * FROM EXAMPLE")) { ResultSet rs = query.executeQuery(); while (rs.next()) { System.out.println(String.format("%s, %s!", rs.getString(1), rs.getString("TARGET"))); } rs.close(); } } catch (SQLException ex) { System.out.println("Database connection failure: " + ex.getMessage()); } finishLatch.countDown(); }; IntStream.range(0, NO_THREADS).forEach( (index) -> new Thread(readingThread).start() ); |
Я запускаю 16 потоков, которые ждут запуска друг друга и потом одновременно начинают выполнять один и тот же запрос. А в этом время, в PostgreSQL можно наблюдать, как 16 потоков открывают 16 соединений и делают 16 запросов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | test=# select datname, usename, client_addr, client_port, query from pg_stat_activity; datname | usename | client_addr | client_port | query ---------+----------+--------------+-------------+--------------------------------------------------------------------------------- test | postgres | | -1 | select datname, usename, client_addr, client_port, query from pg_stat_activity; test | test | 192.168.75.1 | 56777 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56778 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56779 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56780 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56781 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56782 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56783 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56784 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56785 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56786 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56786 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56786 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56786 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56786 | SELECT * FROM EXAMPLE test | test | 192.168.75.1 | 56786 | SELECT * FROM EXAMPLE (17 строк) |
Код примера доступен на github. Для запуска примера требуется установить PostgreSQL сервер и разрешить к нему доступ. Если сервер будет установлен не на локальной машине, требуется изменить его адрес в jdbc url.