1. Información general
La agrupación de conexiones es un patrón de acceso a datos bien conocido, cuyo objetivo principal es reducir la sobrecarga involucrada en la realización de conexiones de base de datos y operaciones de base de datos de lectura / escritura.
En pocas palabras, un grupo de conexiones es, en el nivel más básico, una implementación de caché de conexión de base de datos , que se puede configurar para adaptarse a requisitos específicos.
En este tutorial, haremos un resumen rápido de algunos marcos de agrupación de conexiones populares y aprenderemos cómo implementar desde cero nuestro propio grupo de conexiones.
2. ¿Por qué Connection Pooling?
La pregunta es retórica, por supuesto.
Si analizamos la secuencia de pasos involucrados en un ciclo de vida de conexión de base de datos típico, entenderemos por qué:
- Abrir una conexión a la base de datos usando el controlador de la base de datos
- Abrir un conector TCP para leer / escribir datos
- Leer / escribir datos a través del zócalo
- Cerrando la conexión
- Cerrando el enchufe
Se hace evidente que las conexiones a la base de datos son operaciones bastante costosas y, como tales, deben reducirse al mínimo en todos los casos de uso posibles (en los casos extremos, simplemente evitarlos).
Aquí es donde entran en juego las implementaciones de agrupación de conexiones.
Simplemente implementando un contenedor de conexión de base de datos, que nos permite reutilizar una cantidad de conexiones existentes, podemos ahorrar efectivamente el costo de realizar una gran cantidad de costosos viajes de base de datos, lo que aumenta el rendimiento general de nuestras aplicaciones basadas en bases de datos.
3. Marcos de agrupación de conexiones JDBC
Desde una perspectiva pragmática, implementar un grupo de conexiones desde cero no tiene sentido, considerando la cantidad de marcos de trabajo de grupos de conexiones "listos para la empresa" disponibles en el mercado.
Desde el punto de vista didáctico, que es el objetivo de este artículo, no lo es.
Aun así, antes de que aprendamos a implementar un grupo de conexiones básico, primero mostremos algunos marcos de grupos de conexiones populares.
3.1. Apache Commons DBCP
Comencemos este resumen rápido con Apache Commons DBCP Component, un marco JDBC de agrupación de conexiones con todas las funciones:
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(){ } }
En este caso, hemos utilizado una clase contenedora con un bloque estático para configurar fácilmente las propiedades de DBCP.
A continuación, se explica cómo obtener una conexión agrupada con la clase DBCPDataSource :
Connection con = DBCPDataSource.getConnection();
3.2. HikariCP
Continuando, veamos HikariCP, un marco de agrupación de conexiones JDBC ultrarrápido creado por Brett Wooldridge (para obtener todos los detalles sobre cómo configurar y aprovechar al máximo HikariCP, consulte este artículo):
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(){} }
Del mismo modo, aquí se explica cómo obtener una conexión agrupada con la clase HikariCPDataSource :
Connection con = HikariCPDataSource.getConnection();
3.3. C3PO
El último de esta revisión es C3PO, un poderoso marco de conexión JDBC4 y combinación de declaraciones desarrollado por 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(){} }
Como se esperaba, obtener una conexión agrupada con la clase C3poDataSource es similar a los ejemplos anteriores:
Connection con = C3poDataSource.getConnection();
4. Una implementación simple
Para comprender mejor la lógica subyacente de la agrupación de conexiones, creemos una implementación simple.
Comencemos con un diseño de acoplamiento flexible, basado en una sola interfaz:
public interface ConnectionPool { Connection getConnection(); boolean releaseConnection(Connection connection); String getUrl(); String getUser(); String getPassword(); }
The ConnectionPool interface defines the public API of a basic connection pool.
Now, let's create an implementation, which provides some basic functionality, including getting and releasing a pooled connection:
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 }
While pretty naive, the BasicConnectionPool class provides the minimal functionality that we'd expect from a typical connection pooling implementation.
In a nutshell, the class initializes a connection pool based on an ArrayList that stores 10 connections, which can be easily reused.
It's possible to create JDBC connections with the DriverManager class and with Datasource implementations.
As it's much better to keep the creation of connections database agnostic, we've used the former, within the create() static factory method.
In this case, we've placed the method within the BasicConnectionPool, because this is the only implementation of the interface.
In a more complex design, with multiple ConnectionPool implementations, it'd be preferable to place it in the interface, therefore getting a more flexible design and a greater level of cohesion.
The most relevant point to stress here is that once the pool is created, connections are fetched from the pool, so there's no need to create new ones.
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.
Como mantendremos las cosas simples, omitiremos cómo implementar estas características adicionales y mantendremos la implementación no segura para subprocesos en aras de la claridad.
7. Conclusión
En este artículo, analizamos en profundidad qué es la agrupación de conexiones y aprendimos cómo implementar nuestra propia implementación de agrupación de conexiones.
Por supuesto, no tenemos que empezar desde cero cada vez que queremos agregar una capa de agrupación de conexiones con todas las funciones a nuestras aplicaciones.
Es por eso que primero hicimos un resumen simple que muestra algunos de los marcos de grupo de conexiones más populares, para que podamos tener una idea clara de cómo trabajar con ellos y elegir el que mejor se adapte a nuestros requisitos.
Como de costumbre, todos los ejemplos de código que se muestran en este artículo están disponibles en GitHub.