Um guia simples para o pool de conexões em Java
1. Visão geral
O pool de conexões é um padrão de acesso a dados conhecido, cujo principal objetivo é reduzir a sobrecarga envolvida na execução de conexões com o banco de dados e operações de leitura / gravação no banco de dados.
Resumindo,a connection pool is, at the most basic level, a database connection cache implementation, que pode ser configurado para atender a requisitos específicos.
Neste tutorial, faremos um resumo rápido de algumas estruturas de pool de conexões populares e aprenderemos como implementar do zero nosso próprio pool de conexões.
2. Por que pool de conexão?
A questão é retórica, é claro.
Se analisarmos a sequência de etapas envolvidas em um ciclo de vida típico de conexão de banco de dados, entenderemos por quê:
-
Abrindo uma conexão com o banco de dados usando o driver de banco de dados
-
Abrindo umTCP socket para leitura / gravação de dados
-
Leitura / gravação de dados no soquete
-
Fechando a conexão
-
Fechando o soquete
Torna-se evidente quedatabase connections are fairly expensive operations, e como tal, deve ser reduzido ao mínimo em todos os casos de uso possíveis (em casos extremos, apenas evitados).
É aqui que as implementações de pool de conexão entram em jogo.
Simplesmente implementando um contêiner de conexão de banco de dados, o que nos permite reutilizar várias conexões existentes, podemos economizar efetivamente o custo de realizar um grande número de viagens caras ao banco de dados, aumentando assim o desempenho geral de nossos aplicativos orientados a banco de dados.
3. JDBC Connection Pooling Frameworks
De uma perspectiva pragmática, a implementação de um pool de conexões desde o início é inútil, considerando o número de estruturas de pool de conexões “prontas para a empresa” disponíveis no mercado.
Do lado didático, que é o objetivo deste artigo, não é.
Mesmo assim, antes de aprendermos como implementar um pool de conexão básico, vamos primeiro mostrar algumas estruturas populares de pool de conexão.
3.1. Apache Commons DBCP
Vamos começar este rápido resumo comApache Commons DBCP Component, uma estrutura JDBC de pooling de conexões com todos os recursos:
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(){ }
}
Neste caso, usamos uma classe wrapper com um bloco estático para configurar facilmente as propriedades do DBCP.
Veja como obter uma conexão agrupada com a classeDBCPDataSource:
Connection con = DBCPDataSource.getConnection();
3.2. HikariCP
Continuando, vamos dar uma olhada emHikariCP, uma estrutura de pool de conexão JDBC extremamente rápida criada porBrett Wooldridge (para os detalhes completos sobre como configurar e obter o máximo do HikariCP, verifiquethis 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(){}
}
Da mesma forma, aqui está como obter uma conexão em pool com a classeHikariCPDataSource:
Connection con = HikariCPDataSource.getConnection();
3.3. C3PO
O último desta revisão éC3PO, uma poderosa conexão JDBC4 e framework de pool de instruções desenvolvido 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 esperado, obter uma conexão agrupada com a classeC3poDataSource é semelhante aos exemplos anteriores:
Connection con = C3poDataSource.getConnection();
4. Uma implementação simples
Para entender melhor a lógica subjacente do pool de conexão, vamos criar uma implementação simples.
Vamos começar com um design fracamente acoplado, com base em apenas uma única interface:
public interface ConnectionPool {
Connection getConnection();
boolean releaseConnection(Connection connection);
String getUrl();
String getUser();
String getPassword();
}
A interfaceConnectionPool define a API pública de um pool de conexão básico.
Agora, vamos criar uma implementação, que fornece algumas funcionalidades básicas, incluindo obter e liberar uma conexão agrupada:
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
}
Embora bastante ingênua, a classeBasicConnectionPool fornece a funcionalidade mínima que esperaríamos de uma implementação típica de pool de conexão.
Em suma, a classe inicializa um pool de conexão com base em umArrayList que armazena 10 conexões, que podem ser facilmente reutilizadas.
It’s possible to create JDBC connections with the DriverManager class and with Datasource implementations.
Como é muito melhor manter a criação de banco de dados de conexões agnóstica, usamos o primeiro, dentro do método de fábrica estáticocreate().
Neste caso, colocamos o método dentro deBasicConnectionPool, porque esta é a única implementação da interface.
Em um design mais complexo, com várias implementações deConnectionPool, seria preferível colocá-lo na interface, obtendo assim um design mais flexível e um maior nível de coesão.
O ponto mais relevante a enfatizar aqui é que, uma vez que o pool é criado,connections are fetched from the pool, so there’s no need to create new ones.
Além disso,when a connection is released, it’s actually returned back to the pool, so other clients can reuse it.
Não há mais nenhuma interação com o banco de dados subjacente, como uma chamada explícita ao métodoConnection’s close().
5. Usando a classeBasicConnectionPool
Como esperado, usar nossa classeBasicConnectionPool é simples.
Vamos criar um teste de unidade simples e obter uma conexão in-memoryH2 em pool:
@Test
public whenCalledgetConnection_thenCorrect() {
ConnectionPool connectionPool = BasicConnectionPool
.create("jdbc:h2:mem:test", "user", "password");
assertTrue(connectionPool.getConnection().isValid(1));
}
6. Outras melhorias e refatoração
Claro, há muito espaço para ajustar / estender a funcionalidade atual de nossa implementação de pool de conexão.
Por exemplo, poderíamos refatorar o métodogetConnection() e adicionar suporte para o tamanho máximo do pool. Se todas as conexões disponíveis forem realizadas e o tamanho atual do pool for menor que o máximo configurado, o método criará uma nova conexão:
@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;
}
Observe que o método agora lançaSQLException, o que significa que teremos que atualizar a assinatura da interface também.
Ou, podemos adicionar um método para encerrar normalmente nossa instância do pool de conexão:
public void shutdown() throws SQLException {
usedConnections.forEach(this::releaseConnection);
for (Connection c : connectionPool) {
c.close();
}
connectionPool.clear();
}
Nas implementações prontas para produção, um pool de conexões deve fornecer vários recursos extras, como a capacidade de rastrear as conexões atualmente em uso, suporte para o pool de instruções preparado e assim por diante.
Como vamos manter as coisas simples, vamos omitir como implementar esses recursos adicionais e manter a implementação sem thread-safe por uma questão de clareza.
7. Conclusão
Neste artigo, analisamos detalhadamente o que é o pool de conexões e aprendemos como rolar nossa própria implementação de pool de conexões.
Claro, não temos que começar do zero sempre que quisermos adicionar uma camada de pooling de conexão com todos os recursos aos nossos aplicativos.
É por isso que fizemos primeiro um resumo simples mostrando algumas das estruturas de pool de conexão mais populares, para que possamos ter uma ideia clara de como trabalhar com elas e escolher aquela que melhor atende aos nossos requisitos.
Como de costume, todos os exemplos de código mostrados neste artigo estão disponíveisover on GitHub.