Простое руководство по пулу соединений в Java

Простое руководство по пулу соединений в Java

1. обзор

Пул соединений является хорошо известным шаблоном доступа к данным, основная цель которого состоит в том, чтобы уменьшить накладные расходы, связанные с выполнением соединений с базой данных и операциями чтения / записи с базой данных.

Вкратце,a connection pool is, at the most basic level, a database connection cache implementation, который можно настроить в соответствии с конкретными требованиями.

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

2. Почему пул соединений?

Вопрос риторический, конечно.

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

  1. Открытие соединения с базой данных с помощью драйвера базы данных

  2. ОткрытиеTCP socket для чтения / записи данных

  3. Чтение / запись данных через сокет

  4. Закрытие соединения

  5. Закрытие розетки

Становится очевидным, чтоdatabase connections are fairly expensive operations, и как таковой, следует свести к минимуму во всех возможных случаях использования (в крайних случаях просто избегать).

Здесь в игру вступают реализации пула соединений.

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

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, созданнуюBrett Wooldridge (для получения полной информации о том, как настроить и получить максимальную отдачу от HikariCP, проверьтеthis article ):

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. C3PO

Последний в этом обзоре -C3PO, мощный фреймворк JDBC4 для соединений и пулов операторов, разработанный Стивом Уолдманом:

public class C3poDataSource {

    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 C3poDataSource(){}
}

Как и ожидалось, получение объединенного соединения с классомC3poDataSource аналогично предыдущим примерам:

Connection con = C3poDataSource.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 connectionPool;
    private List usedConnections = new ArrayList<>();
    private static int INITIAL_POOL_SIZE = 10;

    public static BasicConnectionPool create(
      String url, String user,
      String password) throws SQLException {

        List 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 соединений, которые можно легко использовать повторно.

It’s possible to create JDBC connections with the DriverManager class and with Datasource implementations.

Поскольку гораздо лучше сохранить независимость создания базы данных соединений от базы данных, мы использовали первое в рамках статического метода фабрикиcreate().

В этом случае мы поместили метод вBasicConnectionPool, потому что это единственная реализация интерфейса.

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

Здесь наиболее важно подчеркнуть, что после создания пулаconnections are fetched from the pool, so there’s no need to create new ones.

Кроме того,when a connection is released, it’s actually returned back to the pool, so other clients can reuse it.

Никакого дальнейшего взаимодействия с базовой базой данных, например явного вызова методаConnection’s close(), не происходит.

5. Использование классаBasicConnectionPool

Как и ожидалось, использовать наш классBasicConnectionPool просто.

Давайте создадим простой модульный тест и получим объединенное соединение in-memoryH2:

@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);
    usedConnections.add(connection);
    return connection;
}

Обратите внимание, что метод теперь выдаетSQLException, то есть нам также нужно обновить подпись интерфейса.

Или мы можем добавить метод для корректного завершения работы нашего экземпляра пула соединений:

public void shutdown() throws SQLException {
    usedConnections.forEach(this::releaseConnection);
    for (Connection c : connectionPool) {
        c.close();
    }
    connectionPool.clear();
}

В готовых к реализации реализациях пул соединений должен предоставлять ряд дополнительных функций, таких как возможность отслеживания соединений, которые используются в настоящее время, поддержка подготовленного пула операторов и т. Д.

Чтобы упростить задачу, мы опустим, как реализовать эти дополнительные функции, и для ясности оставим реализацию небезопасной для потоков.

7. Заключение

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

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

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

Как обычно, доступны все примеры кода, показанные в этой статьеover on GitHub.