При работе с 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.