Un guide simple sur le regroupement de connexions en Java

Un guide simple sur le regroupement de connexions en Java

1. Vue d'ensemble

Le regroupement de connexions est un modèle d'accès aux données bien connu, dont l'objectif principal est de réduire le temps système nécessaire à la réalisation de connexions de base de données et aux opérations de lecture / écriture de base de données.

En un mot,a connection pool is, at the most basic level, a database connection cache implementation, qui peut être configuré pour répondre à des exigences spécifiques.

Dans ce didacticiel, nous allons faire un tour d'horizon rapide de quelques frameworks de pool de connexions populaires, et nous allons apprendre à implémenter à partir de zéro notre propre pool de connexions.

2. Pourquoi le pool de connexions?

La question est rhétorique, bien sûr.

Si nous analysons la séquence des étapes impliquées dans un cycle de vie de connexion à une base de données typique, nous comprendrons pourquoi:

  1. Ouverture d'une connexion à la base de données à l'aide du pilote de base de données

  2. Ouverture d'unTCP socket pour lire / écrire des données

  3. Lecture / écriture de données sur le socket

  4. Fermer la connexion

  5. Fermer la prise

Il devient évident quedatabase connections are fairly expensive operations, et en tant que tel, doit être réduit au minimum dans chaque cas d'utilisation possible (dans les cas extrêmes, juste évité).

Voici où les implémentations de regroupement de connexions entrent en jeu.

En mettant simplement en œuvre un conteneur de connexion à la base de données, ce qui nous permet de réutiliser un certain nombre de connexions existantes, nous pouvons économiser efficacement le coût lié à l'exécution d'un grand nombre de déplacements coûteux de la base de données, améliorant ainsi les performances globales de nos applications pilotées par base de données.

3. Structures de regroupement de connexions JDBC

D'un point de vue pragmatique, la mise en œuvre d'un pool de connexions à partir de la base est tout simplement inutile, compte tenu du nombre de cadres de mise en pool de connexions «prêts pour l'entreprise» disponibles.

D'un point de vue didactique, qui est le but de cet article, ce n'est pas le cas.

Même dans ce cas, avant d'apprendre à implémenter un pool de connexions de base, commençons par présenter quelques frameworks de pool de connexions populaires.

3.1. Apache Commons DBCP

Commençons ce rapide tour d'horizon avecApache Commons DBCP Component, un framework JDBC de regroupement de connexions complet:

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

Dans ce cas, nous avons utilisé une classe wrapper avec un bloc statique pour configurer facilement les propriétés de DBCP.

Voici comment obtenir une connexion groupée avec la classeDBCPDataSource:

Connection con = DBCPDataSource.getConnection();

3.2. HikariCP

Passons maintenant àHikariCP, un framework de regroupement de connexions JDBC ultra-rapide créé parBrett Wooldridge (pour plus de détails sur la façon de configurer et de tirer le meilleur parti de HikariCP, veuillez vérifierthis 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(){}
}

De même, voici comment obtenir une connexion groupée avec la classeHikariCPDataSource:

Connection con = HikariCPDataSource.getConnection();

3.3. C3PO

Le dernier de cette revue estC3PO, un puissant framework de connexion JDBC4 et de regroupement d'instructions développé par Steve Waldman:

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

Comme prévu, l'obtention d'une connexion groupée avec la classeC3poDataSource est similaire aux exemples précédents:

Connection con = C3poDataSource.getConnection();

4. Une mise en œuvre simple

Pour mieux comprendre la logique sous-jacente du regroupement de connexions, créons une implémentation simple.

Commençons par une conception faiblement couplée, basée sur une seule interface:

public interface ConnectionPool {
    Connection getConnection();
    boolean releaseConnection(Connection connection);
    String getUrl();
    String getUser();
    String getPassword();
}

L'interfaceConnectionPool définit l'API publique d'un pool de connexions de base.

Maintenant, créons une implémentation, qui fournit des fonctionnalités de base, notamment l'obtention et la libération d'une connexion groupée:

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
}

Bien qu’elle soit assez naïve, la classeBasicConnectionPool fournit les fonctionnalités minimales que nous attendions d’une implémentation typique de regroupement de connexions.

En un mot, la classe initialise un pool de connexions basé sur unArrayList qui stocke 10 connexions, qui peuvent être facilement réutilisées.

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

Comme il est préférable de garder la création de connexions indépendantes de la base de données, nous avons utilisé la première, dans la méthode de fabrique statique decreate().

Dans ce cas, nous avons placé la méthode dans lesBasicConnectionPool, car c'est la seule implémentation de l'interface.

Dans une conception plus complexe, avec plusieurs implémentationsConnectionPool, il serait préférable de la placer dans l'interface, obtenant ainsi une conception plus flexible et un plus grand niveau de cohésion.

Le point le plus pertinent à souligner ici est qu'une fois le pool créé,connections are fetched from the pool, so there’s no need to create new ones.

De plus,when a connection is released, it’s actually returned back to the pool, so other clients can reuse it.

Il n'y a plus d'interaction avec la base de données sous-jacente, comme un appel explicite à la méthodeConnection’s close().

5. Utilisation de la classeBasicConnectionPool

Comme prévu, l'utilisation de notre classeBasicConnectionPool est simple.

Créons un test unitaire simple et obtenons une connexion poolée in-memoryH2:

@Test
public whenCalledgetConnection_thenCorrect() {
    ConnectionPool connectionPool = BasicConnectionPool
      .create("jdbc:h2:mem:test", "user", "password");

    assertTrue(connectionPool.getConnection().isValid(1));
}

6. Améliorations supplémentaires et refactoring

Bien sûr, il y a beaucoup de place pour modifier / étendre les fonctionnalités actuelles de notre implémentation de regroupement de connexions.

Par exemple, nous pourrions refactoriser la méthodegetConnection() et ajouter la prise en charge de la taille maximale du pool. Si toutes les connexions disponibles sont prises et que la taille du pool actuel est inférieure au maximum configuré, la méthode créera une nouvelle connexion:

@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;
}

Notez que la méthode lance désormaisSQLException, ce qui signifie que nous devrons également mettre à jour la signature de l'interface.

Ou, nous pourrions ajouter une méthode pour fermer en douceur notre instance de pool de connexions:

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

Dans les mises en œuvre prêtes à la production, un pool de connexions devrait fournir un ensemble de fonctionnalités supplémentaires, telles que la possibilité de suivre les connexions actuellement utilisées, la prise en charge du regroupement d'instructions préparées, etc.

Comme nous allons garder les choses simples, nous allons omettre comment mettre en œuvre ces fonctionnalités supplémentaires et garder la mise en œuvre non thread-safe pour des raisons de clarté.

7. Conclusion

Dans cet article, nous avons examiné en détail ce qu'est le pooling de connexions et avons appris à déployer notre propre implémentation de pooling de connexions.

Bien entendu, nous n’avons pas à repartir de zéro chaque fois que nous souhaitons ajouter une couche de regroupement de connexions complète à nos applications.

C'est pourquoi nous avons d'abord fait un tour d'horizon simple montrant certains des cadres de pool de connexions les plus populaires, afin que nous puissions avoir une idée claire sur la façon de travailler avec eux et choisir celui qui correspond le mieux à nos besoins.

Comme d'habitude, tous les exemples de code présentés dans cet article sont disponiblesover on GitHub.