1. Обзор
Пул соединений — это хорошо известный шаблон доступа к данным, основная цель которого — уменьшить накладные расходы, связанные с выполнением соединений с базой данных и операциями чтения и записи базы данных.
В двух словах, пул соединений — это на самом базовом уровне реализация кэша соединений с базой данных , которую можно настроить в соответствии с конкретными требованиями.
В этом руководстве мы кратко рассмотрим несколько популярных фреймворков пулов соединений и узнаем, как с нуля реализовать собственный пул соединений.
2. Зачем нужен пул соединений?
Вопрос риторический, конечно.
Если мы проанализируем последовательность шагов типичного жизненного цикла подключения к базе данных, мы поймем, почему:
- Открытие соединения с базой данных с помощью драйвера базы данных
- Открытие сокета TCP для чтения/записи данных
- Чтение/запись данных через сокет
- Закрытие соединения
- Закрытие сокета
Становится очевидным, что подключения к базе данных являются довольно дорогостоящими операциями , и поэтому их следует свести к минимуму во всех возможных случаях использования (в крайних случаях их просто следует избегать).
Вот где в игру вступают реализации пула соединений.
Просто внедрив контейнер подключения к базе данных, который позволяет нам повторно использовать ряд существующих подключений, мы можем эффективно снизить затраты на выполнение огромного количества дорогостоящих обращений к базе данных, тем самым повысив общую производительность наших приложений, управляемых базой данных.
3. Среды пула соединений JDBC
С прагматической точки зрения внедрение пула соединений с нуля просто бессмысленно, учитывая количество доступных фреймворков для пулов соединений, готовых к работе в масштабах предприятия.
Из дидактической, которая является целью этой статьи, нет.
Тем не менее, прежде чем мы узнаем, как реализовать базовый пул соединений, давайте сначала продемонстрируем несколько популярных сред пула соединений.
3.1. Apache Commons DBCP
Давайте начнем этот краткий обзор с Apache Commons DBCP Component , полнофункциональной среды JDBC для пула соединений:
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(){ }
}
В этом случае мы использовали класс-оболочку со статическим блоком, чтобы легко настроить свойства DBCP.
Вот как получить объединенное соединение с классом DBCPDataSource
:
Connection con = DBCPDataSource.getConnection();
3.2. HikariCP
Двигаясь дальше, давайте посмотрим на HikariCP , молниеносный фреймворк пула соединений JDBC, созданный Бреттом Вулдриджем (полную информацию о том, как настроить и получить максимальную отдачу от 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(){}
}
Точно так же вот как получить объединенное соединение с классом HikariCPDataSource
:
Connection con = HikariCPDataSource.getConnection();
3.3. C3P0
Последним в этом обзоре является C3P0 , мощная среда соединения JDBC4 и пула операторов, разработанная Стивом Уолдманом:
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(){}
}
Как и ожидалось, получение объединенного соединения с классом C3p0DataSource
аналогично предыдущим примерам:
Connection con = C3p0DataSource.getConnection();
4. Простая реализация
Чтобы лучше понять базовую логику пула соединений, давайте создадим простую реализацию.
Давайте начнем со слабосвязанного дизайна, основанного только на одном интерфейсе:
public interface ConnectionPool {
Connection getConnection();
boolean releaseConnection(Connection connection);
String getUrl();
String getUser();
String getPassword();
}
Интерфейс ConnectionPool
определяет общедоступный API базового пула соединений.
Теперь давайте создадим реализацию, которая предоставляет некоторые базовые функции, включая получение и освобождение соединения из пула:
public class BasicConnectionPool
implements ConnectionPool {
private String url;
private String user;
private String password;
private List<Connection> connectionPool;
private List<Connection> usedConnections = new ArrayList<>();
private static int INITIAL_POOL_SIZE = 10;
public static BasicConnectionPool create(
String url, String user,
String password) throws SQLException {
List<Connection> pool = new ArrayList<>(INITIAL_POOL_SIZE);
for (int i = 0; i < INITIAL_POOL_SIZE; i++) {
pool.add(createConnection(url, user, password));
}
return new BasicConnectionPool(url, user, password, pool);
}
// standard constructors
@Override
public Connection getConnection() {
Connection connection = connectionPool
.remove(connectionPool.size() - 1);
usedConnections.add(connection);
return connection;
}
@Override
public boolean releaseConnection(Connection connection) {
connectionPool.add(connection);
return usedConnections.remove(connection);
}
private static Connection createConnection(
String url, String user, String password)
throws SQLException {
return DriverManager.getConnection(url, user, password);
}
public int getSize() {
return connectionPool.size() + usedConnections.size();
}
// standard getters
}
Несмотря на простоту, класс BasicConnectionPool
предоставляет минимальную функциональность, которую мы ожидаем от типичной реализации пула соединений.
В двух словах, класс инициализирует пул соединений на основе списка ArrayList
, в котором хранятся 10 соединений, которые можно легко использовать повторно.
Можно создавать соединения JDBC с классом DriverManager
и с реализациями источника данных .
Так как гораздо лучше сохранять создание базы данных соединений независимой, мы использовали первую в статическом фабричном методе create() .
В данном случае мы поместили метод в BasicConnectionPool
, потому что это единственная реализация интерфейса.
В более сложном дизайне с несколькими реализациями ConnectionPool
было бы предпочтительнее разместить его в интерфейсе, что дает более гибкий дизайн и более высокий уровень связности.
Здесь наиболее важным моментом является то, что после создания пула соединения извлекаются из пула, поэтому нет необходимости создавать новые .
Кроме того, когда соединение освобождается, оно фактически возвращается обратно в пул, чтобы другие клиенты могли его повторно использовать .
Дальнейшее взаимодействие с базовой базой данных, например явный вызов метода close() соединения , отсутствует.
5. Использование класса BasicConnectionPool
Как и ожидалось, использовать наш класс BasicConnectionPool
несложно.
Давайте создадим простой модульный тест и получим объединенное соединение H2 в памяти :
@Test
public whenCalledgetConnection_thenCorrect() {
ConnectionPool connectionPool = BasicConnectionPool
.create("jdbc:h2:mem:test", "user", "password");
assertTrue(connectionPool.getConnection().isValid(1));
}
6. Дальнейшие улучшения и рефакторинг
Конечно, есть много возможностей для настройки/расширения текущей функциональности нашей реализации пула соединений.
Например, мы могли бы реорганизовать метод getConnection()
и добавить поддержку максимального размера пула. Если все доступные подключения заняты, а текущий размер пула меньше настроенного максимума, метод создаст новое подключение.
Кроме того, мы могли бы дополнительно проверить, живо ли соединение, полученное из пула, перед передачей его клиенту.
@Override
public Connection getConnection() throws SQLException {
if (connectionPool.isEmpty()) {
if (usedConnections.size() < MAX_POOL_SIZE) {
connectionPool.add(createConnection(url, user, password));
} else {
throw new RuntimeException(
"Maximum pool size reached, no available connections!");
}
}
Connection connection = connectionPool
.remove(connectionPool.size() - 1);
if(!connection.isValid(MAX_TIMEOUT)){
connection = createConnection(url, user, password);
}
usedConnections.add(connection);
return connection;
}
Обратите внимание, что метод теперь выдает SQLException
, а это означает, что нам также придется обновить сигнатуру интерфейса.
Или мы могли бы добавить метод для корректного закрытия нашего экземпляра пула соединений:
public void shutdown() throws SQLException {
usedConnections.forEach(this::releaseConnection);
for (Connection c : connectionPool) {
c.close();
}
connectionPool.clear();
}
В готовых к работе реализациях пул соединений должен предоставлять ряд дополнительных функций, таких как возможность отслеживать соединения, которые используются в данный момент, поддержку пула подготовленных операторов и т. д.
Поскольку мы не будем усложнять задачу, мы опустим, как реализовать эти дополнительные функции, и оставим реализацию небезопасной для потоков для ясности.
7. Заключение
В этой статье мы подробно рассмотрели, что такое пул соединений, и узнали, как развернуть нашу собственную реализацию пула соединений.
Конечно, нам не нужно начинать с нуля каждый раз, когда мы хотим добавить в наши приложения полнофункциональный уровень пула соединений.
Вот почему мы сначала сделали простой обзор, показывающий некоторые из самых популярных фреймворков пула соединений, чтобы мы могли иметь четкое представление о том, как с ними работать, и выбрать тот, который лучше всего соответствует нашим требованиям.
Как обычно, все примеры кода, показанные в этой статье, доступны на GitHub .