7.1 Пул соединений (Connection pool)

Сегодня мы научимся работать с базой данных еще более профессионально. И поговорим мы сейчас о пуле потоков, или connection pool по-английски.

Коннект к базе данных требует затрат определенного времени. Особенно если база данных удаленная. Если под каждый запрос делать подключение к базе, то отклик нашего приложения будет невероятно низким по скорости. Не говоря уже о ресурсах, которые оно потребит.

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

Когда мы запрашиваем новое соединение, то connection pool его создаёт, а при закрытии он не закрывает его, а сохраняет в connection pool. И если мы снова запросим соединение у connection pool, то он даст нам одно из старых вместо того, чтобы создавать новое.

По сути приложение работает через специальный драйвер, который является оберткой для обычного JDBC-драйвера и который имеет дополнительный функционал по работе с пулом. Эту ситуацию можно представить такой картинкой:

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

Для особенно экстремальных ситуаций можно написать свой connection pool: класс, который буде иметь список соединений. У него будет переопределена функция close, которая будет возвращать соединение обратно в список, и будет много других плюшек вроде таймера открытого соединения. Когда нет конекшина долгое время, соединение закрывается.

7.2* Интерфейс DataSource и ConnectionPoolDataSource

Пул потоков обычно работает в паре с объектом DataSource. Этот объект можно рассматривать как некую абстракцию удаленной базы данных. Само название Data Source можно дословно перевести как Источник Данных. Что как бы намекает.

DataSource-объекты используются для получения физического соединения с базой данных и являются альтернативой DriverManager. При этом нет необходимости регистрировать JDBC-драйвер. Нужно только выставить соответствующие параметры для установки соединения и выполнить метод getConnection().

При создании объекта типа DataSource локально (прямо в приложении) параметры соединения задаются соответствующими методами, предусмотренными поставщиком JDBC-драйвера. Эти методы не определены интерфейсом DataSource, так как параметры для соединения с СУБД разных производителей могут отличаться как по типу, так и по количеству (например, не для всех СУБД необходимо указывать тип драйвера или сетевой протокол).

Так, при работе с СУБД Oracle для использования соответствующих set и get методов необходимо получить объект типа OracleDataSource, который является экземпляром одноименного класса, реализующего интерфейс DataSource. Поэтому такой способ создания объектов типа DataSource делает твой код менее переносимым и зависимым от конкретной реализации драйвера.

Ниже приведен пример кода, иллюстрирующий локальное использование объектов типа DataSource.

import java.sql.*;
import javax.sql.*;
import oracle.jdbc.driver.*;
import oracle.jdbc.pool.*;

public class DataSource {
    public static void main(String[] args) {
    	try {
        	OracleDataSource ods = new OracleDataSource();

        	ods.setUser("root");
        	ods.setPassword("secret");
        	ods.setDriverType("thin");
        	ods.setDatabaseName("test");
        	ods.setServerName("localhost");
        	ods.setPortNumber(1521);

        	Connection connection = ods.getConnection();
        	System.out.println("Connection successful!!!");

    	} catch (SQLException se) {
        	se.printStackTrace();
    	}
    	System.out.println("Goodbye!");
	}
}

7.3* Интерфейс JNDI

Полностью возможности объектов типа DataSource проявляются в сочетании с использованием интерфейса JNDI. JNDI — это специальный сервис (что-то вроде каталога) для больших серверных приложений, который позволяет одним сервисам устанавливать соединение с другими.

Использование службы имен и каталогов позволяет хранить заранее созданные системным администратором объекты типа DataSource с предустановленными параметрами соединения. Ниже приведены некоторые стандартные имена свойств (параметров), разработанные компанией Sun:

Имя свойства Тип данных Java Назначение
databaseName String Имя базы данных
serverName String Имя сервера
user String Имя пользователя (ID)
password String Пароль пользователя
portNumber Int Номер порта сервера СУБД

Комбинирование интерфейсов DataSource и JNDI играет ключевую роль в разработке многоуровневых корпоративных приложений, основанных на компонентной технологии J2EE.

В случае использования комбинации интерфейсов DataSource и JNDI в программном коде приложения необходимо только затребовать объект типа DataSource у службы имен и каталогов. При этом детали соединения и зависимый от конкретной реализации драйвера программный код оказывается скрытым от приложения в хранящемся в службе имен и каталогов объекте.

Таким образом совместное использование объектов типа DataSource и JNDI предполагает наличие двух выполняемых независимо друг от друга шагов:

  1. Необходимо сохранить именованный объект типа DataSource в службе имен и каталогов, используя метод Context.bind() пакета javax.naming.
  2. Затребовать в приложении объект типа DataSource у службы имен и каталогов, используя метод Context.lookup(). После чего можно использовать его метод DataSource.getConnection() для получения соединения с базой данных.

Ниже приведен пример совместного использования интерфейса JNDI и объекта типа OracleDataSource.

// Создаем ключевой JNDI объект
Hashtable env = new Hashtable();
env.put (Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContextFactory");
env.put (Context.PROVIDER_URL, "file:JNDI");
Context ctx = new InitialContext(env);

// Получаем объект DataSource по его имени
DataSource ods = (DataSource) ctx.lookup("myDatabase");

// Получаем Connection из объекта DataSource
Connection connection = ods.getConnection();
System.out.println("Connection successful!!!");

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

Неважно, как это работает. Теб нужно просто знать, что с помощью JNDI сервиса ты можешь получить proxy-объект любого сервиса, зарегистрированного в каталоге сервисов. Именно таким образом получают объект DataSource и с его помощью работают с базой данных.

7.4 Примеры Connection Pool’ов

Давай лучше вернемся к тому, с чего начали — к пулам соединений (connections pool).

Java-программисты в своих программах очень часто используют библиотеки, которые содержат в себе различные реализации connection pool’ов. Среди них есть три самых популярных:

  • Apache Commons DBCP
  • C3PO
  • HikariCP

Первым создали хороший connection pool программисты из проекта Apache, он так же используется внутри веб-сервера Tomcat. Пример работы с ним:

public class DBCPDataSource {

    private static BasicDataSource ds = new BasicDataSource();
    static {
    	ds.setUrl("jdbc:h2:mem:test");
    	ds.setUsername("user");
    	ds.setPassword("password");
    	ds.setMinIdle(5);
    	ds.setMaxIdle(10);
    	ds.setMaxOpenPreparedStatements(100);
    }

    public static Connection getConnection() throws SQLException {
    	return ds.getConnection();
    }

    private DBCPDataSource(){ }
}

Второй очень популярный пул — это C3PO. Странные названия, согласен. Название C3PO — это отсылка к имени робота c3po из Звездных Войн. А также CP — это сокращение от Connection Pool.

public class C3p0DataSource {

    private static ComboPooledDataSource cpds = new ComboPooledDataSource();
    static {
    	try {
        	cpds.setDriverClass("org.h2.Driver");
        	cpds.setJdbcUrl("jdbc:h2:mem:test");
        	cpds.setUser("user");
        	cpds.setPassword("password");
    	} catch (PropertyVetoException e) {
            // handle the exception
    	}
    }

    public static Connection getConnection() throws SQLException {
    	return cpds.getConnection();
    }

    private C3p0DataSource(){}
}

С документацией по нему можно ознакомиться на официальной странице.

И наконец самый популярный Connection Pool в наше время — это HikariCP:

public class HikariCPDataSource {

    private static HikariConfig config = new HikariConfig();
    private static HikariDataSource ds;

    static {
    	config.setJdbcUrl("jdbc:h2:mem:test");
    	config.setUsername("user");
    	config.setPassword("password");
    	config.addDataSourceProperty("cachePrepStmts", "true");
    	config.addDataSourceProperty("prepStmtCacheSize", "250");
    	config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
    	ds = new HikariDataSource(config);
    }

    public static Connection getConnection() throws SQLException {
    	return ds.getConnection();
    }

    private HikariCPDataSource(){}
}

Вот его официальная страничка в GitHub.

И хорошая статья по конфигурированию.