Lidando com Concorrência em Java – Lock Pessimista

Partindo do pressuposto que concorrências acontecerão em uma aplicação multithread, nesse post vou descrever de forma simples e intuitiva o lock pessimista. Ele é um mecanismo de controle de acesso a recursos compartilhados, ou seja, quando threads simultâneas acessam o mesmo registro. 🚀

Importante entender também a estratégia de lock otimista e analisar qual se encaixa melhor no seu problema.

Concorrência

Bom, imagina uma aplicação multithread, ou seja, o mesmo recurso pode ser acessado simultaneamente por threads diferentes.

Threads são processos em paralelo que estão rodando no seu programa e cada uma pode estar “disputando” o mesmo dado.

Trazendo pra vida real, é como se você, leitor, tivesse tentando reservar a cadeira E10 da sessão de Velozes e Furiosos do dia 16/11 às 19:00 e Bento, no mesmíssimo instante, está fazendo a mesma reserva, na mesma cadeira, na mesma sessão. Ou seja, vocês estão “disputando” o mesmo recurso do banco de dados. Está aí um clássico exemplo de concorrência.

Pra resolver esse caso temos várias alternativas e uma delas é o uso de Lock.

🔒 O que é Lock Pessimista e Lock Otimista?

De maneira bastante resumida, um lock otimista assume que os conflitos são raros e o lock pessimista assume que os conflitos são comuns.

Tá, mas o que isso significa? Signfica que as estratégias são diferentes para cada um.

O Lock otimista verifica conflitos apenas na hora de atualizar o registro. Então, supondo que eu estou atualizando o registro de versão 2: se na hora de fazer o commit o lock vê que já existe a versão 3, significa que meu registro já não é o mais atual e a atualização para.

Por exemplo, se você começou a editar com versão 2, mas outra transação já criou a versão 3, sua atualização será rejeitada (muito abstrato, eu sei, escrever sobre lock não está sendo fácil, mas no próximo artigo vamos aprofundar no lock otimista).

Lock Pessimista

Aqui assumimos que os conflitos são comuns e por isso o lock pessimista “tranca” o registro.

Podemos dizer que a tentativa de fazer a mesma reserva no cinema é comum, certo? Bom… então vamos implantar um lock pessimista nesse sistema.

Isso significa que a partir de agora sempre que algum dado (a cadeira E10 da sessão de Velozes e Furiosos dia 16/11 as 19:00) estiver sendo disputado (por você e Bento) nós vamos trancar o acesso a essa linha do banco até que quem chegou primeiro finalize a operação que está tentando fazer.

A imagem abaixo mostra em alto nível como é realizado o lock.
Supondo que Bento começou a reserva primeiro, você só poderá fazer modificações (reservar) aquele assento quando Bento terminar ou desistir do processo dele.

lock-pessimista-flow

E em Java usando JPA ficaria assim:

@Transactional
public void processarPedido(Long pedidoId) {
    // 1. A transação começa aqui. O pool de conexões nos empresta uma conexão.

    // 2. Pedimos o lock PESSIMISTIC_WRITE
    Pedido pedido = entityManager.find(
        Pedido.class,
        pedidoId,
        LockModeType.PESSIMISTIC_WRITE
    );

    // 3. O JPA traduz isso para SQL:
    // "SELECT * FROM pedidos WHERE id = ? FOR UPDATE"
    // O Banco de Dados AGORA bloqueia esta linha.

    // 4. NENHUMA outra transação pode escrever (ou ler com FOR UPDATE)
    // esta linha. Elas ficarão na fila, esperando.

    // ... fazemos nossa lógica de negócio ...
    pedido.setStatus("PROCESSADO");

    // 5. A transação faz COMMIT.
    // O lock é FINALMENTE liberado. A conexão é devolvida ao pool.
}

Importante notar aqui que o recurso mais escasso da sua aplicação não é CPU ou memória, é o pool de conexões do banco de dados.
O lock pessimista segura essa conexão durante todo o tempo de vida da transação. Se a sua "lógica de negócio" for lenta (ex: chamar uma API externa), sua aplicação irá parar.
Por isso é importante trabalhar na arquitetura da sua aplicação e não deixar que a transação precise ficar esperando por respostas externas.

Extras do JPA (o LockModeType)

LockModeType não é uma coisa só. A especificação JPA nos dá opções, e a escolha errada tem consequências.

PESSIMISTIC_WRITE (O Lock Exclusivo)

  • SQL: SELECT … FOR UPDATE (na maioria dos dialetos).

  • O que faz: Impede que outras transações façam SELECT … FOR UPDATE E impede que façam UPDATE ou DELETE. É um lock exclusivo total.

  • Quando usar: Este é o padrão. Você vai ler e definitivamente vai escrever na linha.

PESSIMISTIC_READ (O Lock Compartilhado… às vezes)

  • SQL: SELECT … FOR SHARE (ex: PostgreSQL/MySQL 8+) ou … LOCK IN SHARE MODE (MySQL antigo).

  • O que faz: Impede que outras transações façam UPDATE ou DELETE, mas permite que outras transações também leiam com PESSIMISTIC_READ.

  • Quando usar: Cenário mais raro. Você quer garantir que o dado não mude enquanto você lê, mas sabe que outros podem estar lendo ao mesmo tempo sem intenção de escrita.

PESSIMISTIC_FORCE_INCREMENT (O Híbrido)

  • O que faz: Adquire um lock pessimista (FOR UPDATE) e, além disso, força um incremento na coluna @version (a mesma usada pelo lock otimista), mesmo que você não altere nenhum outro campo.

  • Quando usar: Útil se você precisa invalidar caches ou “sinalizar” para sistemas otimistas que algo mudou, mas garantindo isso de forma pessimista.

Dicas Extras:

  1. Considere configurar timeout no seu lock
Map<String, Object> properties = new HashMap<>();
// Define o timeout em milissegundos.
// "javax.persistence.lock.timeout" = 0 (não espere, falhe imediatamente)
// "javax.persistence.lock.timeout" = 5000 (espere 5 seg e lance LockTimeoutException)
properties.put("javax.persistence.lock.timeout", 5000);

Pedido pedido = entityManager.find(Pedido.class, id, LockModeType.PESSIMISTIC_WRITE, properties);
  1. Se você optar pelo locking pessimista, sua transação deve ser cirúrgica. Ela deve ser extremamente rápida, não fazer I/O externo e ter um plano de tratamento para deadlocks (ordem de lock) e timeouts.
Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Python by Structure: Context Managers and the With Statement

Related Posts