Una semplice guida al pool di connessioni in Java

1. Panoramica

Il pool di connessioni è un modello di accesso ai dati ben noto, il cui scopo principale è ridurre l'overhead connesso all'esecuzione di connessioni al database e operazioni di lettura / scrittura del database.

In poche parole, un pool di connessioni è, al livello più elementare, un'implementazione della cache di connessione del database , che può essere configurata per soddisfare requisiti specifici.

In questo tutorial, faremo un rapido riepilogo di alcuni popolari framework di pool di connessioni e impareremo come implementare da zero il nostro pool di connessioni.

2. Perché il pool di connessioni?

La domanda è retorica, ovviamente.

Se analizziamo la sequenza di passaggi coinvolti in un tipico ciclo di vita di una connessione al database, capiremo perché:

  1. Apertura di una connessione al database utilizzando il driver del database
  2. Apertura di un socket TCP per la lettura / scrittura dei dati
  3. Lettura / scrittura di dati tramite il socket
  4. Chiusura della connessione
  5. Chiusura della presa

Diventa evidente che le connessioni al database sono operazioni piuttosto costose e, come tali, dovrebbero essere ridotte al minimo in ogni possibile caso d'uso (nei casi limite, semplicemente evitate).

È qui che entrano in gioco le implementazioni del pool di connessioni.

Semplicemente implementando un contenitore di connessione al database, che ci consente di riutilizzare un numero di connessioni esistenti, possiamo effettivamente risparmiare il costo di eseguire un numero enorme di costosi viaggi nel database, aumentando così le prestazioni complessive delle nostre applicazioni basate su database.

3. Framework di pool di connessioni JDBC

Da una prospettiva pragmatica, implementare un pool di connessioni da zero è semplicemente inutile, considerando il numero di framework di pool di connessioni "pronti per l'azienda" disponibili là fuori.

Da uno didattico, che è l'obiettivo di questo articolo, non lo è.

Anche così, prima di imparare come implementare un pool di connessioni di base, mostriamo prima alcuni framework di pool di connessioni popolari.

3.1. Apache Commons DBCP

Iniziamo questa rapida carrellata con Apache Commons DBCP Component, un framework JDBC con pool di connessioni completo:

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

In questo caso, abbiamo utilizzato una classe wrapper con un blocco statico per configurare facilmente le proprietà di DBCP.

Ecco come ottenere una connessione in pool con la classe DBCPDataSource :

Connection con = DBCPDataSource.getConnection();

3.2. HikariCP

Andando avanti, diamo un'occhiata a HikariCP, un framework di pool di connessioni JDBC velocissimo creato da Brett Wooldridge (per i dettagli completi su come configurare e ottenere il massimo da HikariCP, controlla questo articolo):

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

Allo stesso modo, ecco come ottenere una connessione in pool con la classe HikariCPDataSource :

Connection con = HikariCPDataSource.getConnection();

3.3. C3PO

Ultimo in questa recensione è C3PO, un potente framework di connessione JDBC4 e pool di istruzioni sviluppato da 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(){} }

Come previsto, ottenere una connessione in pool con la classe C3poDataSource è simile agli esempi precedenti:

Connection con = C3poDataSource.getConnection();

4. Una semplice implementazione

Per comprendere meglio la logica sottostante del pool di connessioni, creiamo una semplice implementazione.

Cominciamo con un design debolmente accoppiato, basato su una singola interfaccia:

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

L' interfaccia ConnectionPool definisce l'API pubblica di un pool di connessioni di base.

Ora, creiamo un'implementazione, che fornisce alcune funzionalità di base, tra cui ottenere e rilasciare una connessione in pool:

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 }

Sebbene piuttosto ingenua, la classe BasicConnectionPool fornisce la funzionalità minima che ci si aspetterebbe da una tipica implementazione del pool di connessioni.

In poche parole, la classe inizializza un pool di connessioni basato su un ArrayList che memorizza 10 connessioni, che possono essere facilmente riutilizzate.

È possibile creare connessioni JDBC con la classe DriverManager e con implementazioni Datasource .

Poiché è molto meglio mantenere la creazione di connessioni al database indipendente, abbiamo utilizzato il primo, all'interno del metodo factory statico create () .

In questo caso, abbiamo inserito il metodo all'interno di BasicConnectionPool , perché questa è l'unica implementazione dell'interfaccia.

In una progettazione più complessa, con più implementazioni di ConnectionPool , sarebbe preferibile posizionarlo nell'interfaccia, ottenendo quindi un design più flessibile e un maggiore livello di coesione.

Il punto più rilevante da sottolineare qui è che una volta creato il pool, le connessioni vengono recuperate dal pool, quindi non è necessario crearne di nuove .

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.

Poiché manterremo le cose semplici, ometteremo come implementare queste funzionalità aggiuntive e manterremo l'implementazione non thread-safe per motivi di chiarezza.

7. Conclusione

In questo articolo, abbiamo esaminato in modo approfondito cos'è il pool di connessioni e abbiamo imparato come eseguire il rollio della nostra implementazione del pool di connessioni.

Ovviamente, non dobbiamo ricominciare da capo ogni volta che vogliamo aggiungere un livello di pool di connessioni completo alle nostre applicazioni.

Ecco perché abbiamo fatto prima una semplice carrellata che mostra alcuni dei framework di pool di connessioni più popolari, in modo da avere un'idea chiara su come lavorare con loro e scegliere quello che meglio si adatta alle nostre esigenze.

Come al solito, tutti gli esempi di codice mostrati in questo articolo sono disponibili su GitHub.