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 le principal objectif est de réduire la surcharge impliquée dans l'exécution des connexions de base de données et des opérations de lecture / écriture de base de données.

En un mot, un pool de connexions est, au niveau le plus élémentaire, une implémentation de cache de connexion de base de données , qui peut être configurée 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 apprendrons comment implémenter à partir de zéro notre propre pool de connexions.

2. Pourquoi le regroupement 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'un socket TCP 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 que les connexions aux bases de données sont des opérations assez coûteuses , et en tant que telles, doivent être réduites au minimum dans tous les cas d'utilisation possibles (dans les cas extrêmes, juste évitées).

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

En implémentant simplement un conteneur de connexion à la base de données, qui nous permet de réutiliser un certain nombre de connexions existantes, nous pouvons effectivement économiser le coût de l'exécution d'un grand nombre de voyages de base de données coûteux, améliorant ainsi les performances globales de nos applications basées sur la 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 zéro est tout simplement inutile, compte tenu du nombre de cadres de regroupement de connexions «prêts pour l'entreprise» disponibles.

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

Même ainsi, avant d'apprendre à implémenter un pool de connexions de base, présentons d'abord quelques frameworks de pool de connexions populaires.

3.1. Apache Commons DBCP

Commençons ce rapide tour d'horizon avec Apache 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 classe DBCPDataSource :

Connection con = DBCPDataSource.getConnection();

3.2. HikariCP

Passons maintenant à HikariCP, un framework de regroupement de connexions JDBC ultra-rapide créé par Brett Wooldridge (pour plus de détails sur la façon de configurer et de tirer le meilleur parti de HikariCP, veuillez consulter cet 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 classe HikariCPDataSource :

Connection con = HikariCPDataSource.getConnection();

3.3. C3PO

Le dernier de cette revue est C3PO, un puissant framework de mise en commun de connexions et d'instructions JDBC4 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 classe C3poDataSource 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' interface ConnectionPool 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, y compris 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'assez naïve, la classe BasicConnectionPool fournit les fonctionnalités minimales que nous attendons d'une implémentation typique de regroupement de connexions.

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

Il est possible de créer des connexions JDBC avec la classe DriverManager et avec des implémentations de source de données .

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

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

Dans une conception plus complexe, avec plusieurs implémentations de ConnectionPool , il serait préférable de le 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éé, les connexions sont extraites du pool, il n'est donc pas nécessaire d'en créer de nouvelles .

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

There's no any further interaction with the underlying database, such as an explicit call to the Connection's close() method.

5. Using the BasicConnectionPool Class

As expected, using our BasicConnectionPool class is straightforward.

Let's create a simple unit test and get a pooled in-memory H2 connection:

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

6. Further Improvements and Refactoring

Of course, there's plenty of room to tweak/extend the current functionality of our connection pooling implementation.

For instance, we could refactor the getConnection() method, and add support for maximum pool size. If all available connections are taken, and the current pool size is less than the configured maximum, the method will create a new connection.

Also, we could additionally verify whether the connection obtained from the pool is still alive, before passing it to the client.

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

Note that the method now throws SQLException, meaning we'll have to update the interface signature as well.

Or, we could add a method to gracefully shut down our connection pool instance:

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

In production-ready implementations, a connection pool should provide a bunch of extra features, such as the ability for tracking the connections that are currently in use, support for prepared statement pooling, and so forth.

Comme nous allons garder les choses simples, nous allons omettre comment implémenter ces fonctionnalités supplémentaires et garder l'implémentation non thread-safe pour des raisons de clarté.

7. Conclusion

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

Bien sûr, nous n'avons pas à repartir de zéro chaque fois que nous voulons 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 disponibles à l'adresse over sur GitHub.