DEV Community

Jefferson Quesado
Jefferson Quesado

Posted on • Originally published at computaria.gitlab.io

O que é o hikari pool?

O que é o hikari pool?

Essa simples pergunta em uma publicação no BlueSky me levou a uma explicação que achei bem legal. Vim aqui terminar ela.

No contexto específico estava sendo falado sobre o Hikari Connection Pool. Mas, se o Hikari é um Connection Pool, o que seria um "Pool"?

First things first, conceito de pool

Antes de explicar o que é HikariCP precisamos explicar o que é um connection pool. E pra explicar connection pool, precisamos explicar pool.

Vamos usar uma analogia econômica para isso? Uma analogia econômica histórica cheia de falhas e inacurácias com o mundo real, mas vai, suspende a descrença rapidinho só pela explicação! É auto-contido.

Imagina que você é um lorde/lady na era medieval. Você detém as ferramentas para a realização do trabalho dos camponeses. E você quer que eles trabalhem. Então, como que você garante isso? Se as ferramentas são suas? Você vai precisar entregar as ferramentas para os camponeses, simples.

Então imagine a situação: seu camponês precisa de uma enxada para capinar o terreno, então ele vai lá e pede pra você uma enxada. Você vai dar a enxada pra ele e vida que segue. Mas e se ele não devolver, como que fica o seu estoque de enxadas? Uma hora vai acabar...

Uma alternativa a entregar a enxada é mandar fazer uma enxada. Você é o senhor/a senhora daquelas terras, então você tem acesso ao ferreiro pra fundir o metal no formato de enxada e encaixar num cabo. Mas isso não é algo que você consegue produzir na hora sem que o camponês fique sentado numa sala de espera. Para fazer esse recurso novo, você demanda de tempo e energia descomunais.

Agora, se o camponês devolver a enxada no final do dia, ela fica disponível para outro camponês usar no dia seguinte.

Aqui você está controlando o pool de enxadas. O pool é um padrão de projeto que indica algo que você pode fazer as seguintes ações:

  • pedir um elemento para ele
  • devolver elemento para ele

Outras coisas também comuns de se ter em pools de objetos:

  • capacidade de criar mais objetos, sob demanda, registrando-os no pool
  • capacidade de destruir objetos do pool (ou desassociar ele daquele pool)

Conexão com o banco de dados JDBC

Bem, vamos nos aproximar ao HikariCP. Vamos falar aqui de conexões com banco de dados em Java.

No java, pedimos para estabelecer uma conexão com o banco de dados. Existe a opção de conexão direta, que você precisa entender diretamente sobre quais classes chamar e alguns detalhes, ou então simplesmente se deleitar com a opção de descoberta de serviço.

A priori, para usar descoberta de serviço, o provedor do serviço faz um jeito de cadastrar o que ele está provendo e então o "service discovery" vai atrás de ver quem poderia servir aquela requisição.

Um caso de service discovery: pstmt-null-safe

Eu peguei um caso em que precisava fazer conexões JDBC para falar com o banco de dados. Porém o meu driver de JDBC não aceitava usar nulos como valor, apenas nulos direto nas queries. Então, o que fiz? Um driver em cima do driver!

A ideia geral era o seguinte. Imagina que eu tenha essa consulta que eu quero inserir valores:

INSERT INTO some_table (id, content, parent)
VALUES (?, ?, ?)
Enter fullscreen mode Exit fullscreen mode

Agora imagine que estou lidando com a primeira inserção desse valor no banco. Para isso, preciso deixar com ID=1, CONTENT=first e PARENT=null porque, afinal, não tem nenhum registro pai desse (é o primeiro, afinal).

O que naturalmente seria feito:

try (final var pstmt = conn.prepareStatement(
                """
                INSERT INTO some_table (id, content, parent)
                VALUES (?, ?, ?)
                """)) {

    pstmt.setInt(1, 1);
    pstmt.setString(2, "first");
    pstmt.setNull(3, Types.INTGEGER); // java.sql.Types
    pstmt.executeUpdate(); // de fato altere o valor
}
Enter fullscreen mode Exit fullscreen mode

Quero continuar usando desse jeito, afinal é o jeito idiomático de se usar. E de acordo com CUPID, o I vem de "idiomático". A ideia de ter um código idiomático é justamente de "diminuir a carga mental desnecessária".

Para resolver isso, minha escolha foi: deixar o prepareStatement para o último momento antes do executeUpdate. Então eu armazeno todos os nulos a serem aplicados e, ao perceber que preciso de fato por um nulo, eu rodo uma substituição de string e gero uma nova query, e essa nova query que será de fato executada.

Nesse caso, eu começo com:

INSERT INTO some_table (id, content, parent)
VALUES (?, ?, ?)
Enter fullscreen mode Exit fullscreen mode

Então, tenho de botar esses valores:

INSERT INTO some_table (id, content, parent)
VALUES (?, ?, ?)

-- 1, 'first', NULL
Enter fullscreen mode Exit fullscreen mode

Só que eu não posso de fato usar o nulo, então eu crio uma chave para identificar que a terceira casa é um nulo:

-- (value, value, NULL)
INSERT INTO some_table (id, content, parent)
VALUES (?, ?, NULL)
-- 1, 'first'
Enter fullscreen mode Exit fullscreen mode

E nesse caso preparo essa nova string e coloco os argumentos conforme o que foi requisitado.

Ok, dito isso, como eu conseguia indicar para a minha aplicação que eu precisava usar o meu driver de JDBC? Como eu fazia esse cadastro?

O projeto em questão é Pstmt Null Safe. Basicamente, existe uma magia no classloader do Java que, ao carregar um jar, ele procura por uma pasta de metadados chamada de META-INF. E no caso de driver JDBC, META-INF/services/java.sql.Driver, e eu anotei com a classe que implementa java.sql.Driver: br.com.softsite.pstmtnullsafe.jdbc.PstmtNullSafeDriver.

Segundo a documentação do java.sql.Driver, todo driver deveria criar uma instância de si mesmo e se registrar no DriverManager. Implementei assim:

public static final PstmtNullSafeDriver instance;

static {
    instance = new PstmtNullSafeDriver();
    try {
        DriverManager.registerDriver(instance);
    } catch (SQLException e) {
        e.printStackTrace();
    }
}
Enter fullscreen mode Exit fullscreen mode

Bloco estático se carrega sozinho. E como que sabemos qual a conexão que deveria ser gerenciada pelo meu driver? A chamada se dá através de DriverManager#getConnection(String url). Temos a URL para perguntar para o driver se ele aceita a conexão. A convenção (aqui de novo, o modo idiomático de se usar) é colocar no prefixo do esquema da URL. Como eu quero que o meu driver se conecte em cima de outro driver, fiz nesse equema:

jdbc:pstmt-nullsafe:<url de conexão sem jdbc:>
\__/ \____________/
 |    |
 |    Nome do meu driver
 Padrão para indicar JDBC
Enter fullscreen mode Exit fullscreen mode

Então, para realizar os testes, conectei com o SQLite, e usei o indicar do Xerial para pedir uma conexão em memória através da URI de conexão:

jdbc:sqlite::memory:
Enter fullscreen mode Exit fullscreen mode

Para "envelopar" a conexão, minha convenção indica que eu não repito o jdbc:, então:

jdbc:pstmt-nullsafe:sqlite::memory:
Enter fullscreen mode Exit fullscreen mode

Dissecando a URI acima:

jdbc:pstmt-nullsafe:sqlite::memory:
\__/ \____________/ \____/ \_____/
 |     |             |       |
 JDBC  meu driver    |       em memória, não use arquivo
                    driver do Xerial SQLite
Enter fullscreen mode Exit fullscreen mode

Tá, e como indicar isso? O Driver#acceptsURL deve retornar verdade se eu posso abrir a conexão. Eu poderia só fazer isso:

public static final String PREFIX_URL = "jdbc:pstmt-nullsafe:";

@Override
public boolean acceptsURL(String url) {
    return url.startsWith(PREFIX_URL);
}
Enter fullscreen mode Exit fullscreen mode

Mas o que isso indicaria se eu tentasse carregare um driver inexistente? Nada, iria dar um problema em outro momento. E isso não é bom, o ideal seria dar pane logo no começo. Então pra isso, vou tentar carregar o driver por baixo, e se não conseguir, eu retorno falso:

public static final String PREFIX_URL = "jdbc:pstmt-nullsafe:";

@Override
public boolean acceptsURL(String url) throws SQLException {
    if (url.startsWith(PREFIX_URL)) {
        return getUnderlyingDriver(url) != null;
    }
    return false;
}

private String toUnderlyingUrl(String url) {
    return "jdbc:" + url.substring(PREFIX_URL.length());
}

private Driver getUnderlyingDriver(String url) throws SQLException {
    return DriverManager.getDriver(toUnderlyingUrl(url));
}
Enter fullscreen mode Exit fullscreen mode

O código real do driver tem alguns pontos a mais que não são relevantes a discussão aqui sobre HikariCP, nem sobre DataSource, nem JDBC ou tópicos abordados neste post.

Então, ao requisitar uma conexão "null safe" para o DriverManager, primeiro ele acha o meu driver e o meu driver recursivamente tenta verificar se existe a possibilidade de conexão por debaixo dos panos. Confirmado que existe algum driver capaz de lidar com isso, retorno que sim, é possível.

O padrão de uso de conexões JDBC no Java

A interface Connection implementa a interface AutoCloseable. Isso significa que você pega a conexão, usa a conexão como deseja, e então você fecha a conexão. É bem padrão você usar alguma indireção com isso ou, se usar a conexão diretamente, usar dentro de um bloco try-with-resources:

try (final var conn = getJdbcConnection();
     final var pstmt = conn.prepareStatement( """
                INSERT INTO some_table (id, content, parent)
                VALUES (?, ?, ?)
                """)) {
    // something with the code
}
Enter fullscreen mode Exit fullscreen mode

Agora, o processo de criar conexões é um processo caro. E também o processo de service discovery não é exatamente gratuito. Então o ideal seria guardar o driver para então gerar as conexões. Vamos desenvolver isso aos poucos.

Primeiro, vamos precisar ter um objeto que podemos iniciar com o driver. Pode tranquilamente ser um objeto global, um componente injetado do Spring, ou qualquer coias assim. Chamemos ele de JdbcConnector:

public class JdbcConnector {
    private final String url;
    private final Driver driver;
    private final Properties defaultProperties;

    public static JdbcConnector createJdbcConnector(String url) throws SQLException {
        return createJdbcConnector(url, new Properties());
    }

    public static JdbcConnector createJdbcConnector(String url, Properties defaultProperties) throws SQLException {
        final var driver = DriverManager.getDriver(url);
        if (driver == null) {
            return null;
        }
        return new JdbcConnector(url, driver, defaultProperties);
    }

    private JdbcConnector(Stirng url, Driver driver, Properties defaultProperties) {
        this.url = url;
        this.driver = driver;
        this.defaultProperties = defaultProperties;
    }

    public Connection getConnection() {
        return driver.connect(url, this.defaultProperties);
    }
}
Enter fullscreen mode Exit fullscreen mode

Uma implementação possível para getJdbcConnection() é confiar em um estado englobado por essa função:

private JdbcConnector jdbcConnector = /* inicializa de algum jeito */;

private Connection getJdbcConnection() {
    return jdbcConnector.getConnection();
}
Enter fullscreen mode Exit fullscreen mode

Tudo muito bem até aqui. Mas... lembra do exemplo inicial de que o camponês pede uma enxada no pool de ferramentas? Então... Vamos levar isso em consideração? No lugar de realmente fechar a conexão, podemos devolver a conexão para o pool. Por uma questão de corretude vou proteger contra múltiplos acessos simultâneos, mas não vou me preocupar aqui em eficiência.

Vamos aqui assumir que eu tenho uma classe chamada de ConnectionDelegator. Ela implementa todos os métodos de Connection, porém não faz nada por conta própria, só delega para um connection que é passado pra ela como construtor. Por exemplo, para o método isClosed():

public abstract class ConnectionDelegator implements Connection {

    protected final Connection connection;

    public ConnectionDelegator(Connection connection) {
        this.connection = connection;
    }

    @Overrride
    public boolean isClosed() throws SQLException {
        return connection.isClosed();
    }

    // ... todos os outros métodos
}
Enter fullscreen mode Exit fullscreen mode

E assim para os demais métodos. Ela é abstrata pelo simples fato de que eu quero me forçar a quando for usar fazer algo que não seja uma simples delegação.

Pois bem, vamos lá. A ideia é que vai ser pedida uma conexão, que pode ou não existir. Se ela existir, eu envelopo nessa nova classe para poder depois devolver ao pool quando fechar a conexão. Hmmm, então vou fazer algo no método close()... Tá, vamos envelopar antes. Vamos deixar o getConnection() como synchronized para evitar problemas de concorrência:

private final ArrayList<Connection> pool = new ArrayList<>();

private Connection getConnectionFromDriver() {
    if (pool.isEmpty()) {
        return driver.connect(url, this.defaultProperties);
    }
    return pool.removeLast();
}

public synchronized Connection getConnection() {
    final var availableConnection = getConnectionFromDriver();
    return new ConnectionDelegator(availableConnection) {

        @Override
        public void close() {
            // ...
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

Ok, se eu tiver elementos no pool de conexões eu os uso até ele ficar vazio. Mas ele nunca é preenchido! Pois vamos resolver essa questão? Quando fechar, podemos devolver ao pool!

private synchronized void addToPool(Connection connection) {
    pool.addLast(connection);
}

public synchronized Connection getConnection() {
    final var availableConnection = getConnectionFromDriver();
    return new ConnectionDelegator(availableConnection) {

        @Override
        public void close() {
            addToPool(this.connection);
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

Ok, agora ao terminar de usar a conexão, ela é enviada de volta para
o pool. Isso não satisfaz a documentação do método Connetion#close(), porque na documentação mencional que libera todos os recursos JDBC relacionados a esta conexão. Isso significa que eu precisaria manter um registro de todos Statements, ResultSets, PreparedStatements etc. Podemos lidar com isso criando um método protected em ConnectionDelegator chamado closeAllInnerResources(). E chamar ele no close():

public synchronized Connection getConnection() {
    final var availableConnection = getConnectionFromDriver();
    return new ConnectionDelegator(availableConnection) {

        @Override
        public void close() {
            this.closeAllInnerResources();
            addToPool(this.connection);
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

E com isso temos algo que me devolve conexões sob demanda e que tem a capacidade de formar um pool de recursos.

Sabe qual o nome que o Java dá para um objeto que fornece conexões? DataSource. E sabe o que mais o Java tem a dizer sobre DataSources? Que existem alguns tipos, conceitualmente falando. E que desses tipos os 2 mais relevante são:

  • básico: não faz pooling, pediu conexão só cria e devolve
  • pooled: em que há um pooling de conexões com o banco

E aqui passamos pelo processo justamente de criar conexões sempre (tipo básico) como também evoluímos para um DataSource
pooled.

O que é o HikariCP?

HikariCP é um DataSource. Especificamente, um pooled DataSource. Só que ele tem uma característica: ele é o mais rápido de todos. Para garantir essa velocidade, no pool de conexões dele para uso durante o ciclo de vida da aplicação, o HikariCP faz um segredo: já cria todas as conexões disponíves. Assim, ao chegar um getConnection, o HikariCP só vai precisar verificar o pool de conexões.

Se quiser se aprofundar no assunto, pode consultar este artigo no Baeldung sobre o assunto, e também consultar o repositório no github.

Top comments (2)

Collapse
 
jessilyneh profile image
Jessilyneh

Eu pessoalmente prefiro quando você e outros colegas postam textos assim, porque ai tem mais espaço pra explorar o tema. Muito bom conteúdo!

Collapse
 
cedon profile image
Carlos Nogueira

Demais!