Duas pessoas tentam comprar o último item em estoque ao mesmo tempo.

As duas requisições leem:

estoque = 1

As duas tentam concluir a compra. Sem controle de concorrência, ambas podem considerar a operação válida e o sistema acaba vendendo algo que já não existe.

Esse é o tipo de problema que o lock otimista e o lock pessimista ajudam a resolver.

O problema: ler e depois atualizar

Muitas regras de negócio parecem uma única ação, mas acontecem em várias etapas:

Ler o estoque → Validar a disponibilidade → Atualizar o estoque

Quando duas transações executam esse fluxo ao mesmo tempo, as etapas podem se intercalar:

Transação A: lê estoque = 1
Transação B: lê estoque = 1
Transação A: grava estoque = 0
Transação B: grava estoque = 0

A segunda escrita sobrescreve um estado calculado a partir de informação antiga. Esse problema é conhecido como lost update, ou atualização perdida.

Uma transação no banco não elimina automaticamente esse risco. O resultado depende do nível de isolamento, da consulta executada e de como a aplicação condiciona a escrita.

O que é lock otimista?

O controle otimista assume que duas operações tentando alterar o mesmo dado ao mesmo tempo serão relativamente raras.

Em vez de manter o registro bloqueado desde a leitura, cada operação segue normalmente. No momento da escrita, a aplicação verifica se o dado continua na versão que ela leu.

Apesar do nome, lock otimista geralmente não mantém um lock de banco durante o trabalho. Ele detecta a concorrência comparando uma versão, um timestamp ou os valores anteriores.

Como funciona com uma versão

Imagine este produto:

product_idstockversion
12317

A aplicação lê stock = 1 e version = 7. Para reservar a unidade, executa uma atualização condicional:

UPDATE products
SET stock = stock - 1,
    version = version + 1
WHERE id = 123
  AND version = 7
  AND stock > 0;

Se outra transação atualizar o produto primeiro, a versão deixa de ser 7. A operação afeta zero linhas, e esse resultado é o sinal de que ocorreu um conflito.

A lê versão 7 ─┐
               ├─ A atualiza primeiro → versão 8
B lê versão 7 ─┘
                  B tenta atualizar versão 7 → 0 linhas

A aplicação precisa tratar esse resultado explicitamente. Dependendo da operação, ela pode:

  • Recarregar o dado e tentar novamente;
  • Recalcular o resultado com o estado atual;
  • Rejeitar a ação e informar que o conteúdo mudou;
  • Encaminhar o conflito para uma revisão manual.

Quando o lock otimista funciona bem?

Ele costuma ser uma boa escolha quando:

  • A contenção é baixa;
  • Conflitos são incomuns;
  • Muitas operações leem e poucas escrevem;
  • Tentar novamente é barato e seguro;
  • Não queremos manter uma transação aberta enquanto o usuário trabalha.

É comum em edição de perfis, atualização de catálogos, documentos e workflows distribuídos nos quais alterações simultâneas ao mesmo registro não acontecem com frequência.

Também é útil em interfaces humanas. Não faria sentido manter um lock de banco durante os vários minutos em que uma pessoa edita um formulário.

O que é lock pessimista?

O lock pessimista assume que o conflito é provável — ou que suas consequências são caras demais. A transação adquire um bloqueio antes de tomar a decisão e o mantém até executar COMMIT ou ROLLBACK.

Em um banco relacional, um padrão comum é usar SELECT ... FOR UPDATE:

BEGIN;

SELECT stock
FROM products
WHERE id = 123
FOR UPDATE;

UPDATE products
SET stock = stock - 1
WHERE id = 123
  AND stock > 0;

COMMIT;

Enquanto essa transação mantém o lock, outra transação que tenta adquirir um lock incompatível sobre a mesma linha precisa esperar, falhar imediatamente ou atingir um timeout, conforme o banco e as opções usadas.

Transação A: adquire o lock → valida → atualiza → commit
Transação B:        espera pelo lock        → continua

O detalhe essencial é que a leitura e a escrita precisam fazer parte da mesma transação. Executar SELECT ... FOR UPDATE, encerrar a transação e só depois atualizar remove a proteção que o lock deveria oferecer.

Quando o lock pessimista funciona bem?

Ele pode ser adequado quando:

  • A contenção é alta ou previsível;
  • Operações conflitantes são frequentes;
  • Refazer o trabalho é caro;
  • A decisão depende de várias leituras e escritas relacionadas;
  • A transação pode permanecer curta.

Reservas de assentos em passagens, ingressos, quartos e estoque disputado são exemplos frequentes. Pagamentos também podem exigir serialização em pontos específicos, mas lock não substitui idempotência, unique constraint nem o tratamento de falhas externas.

Comparação direta

CritérioOtimistaPessimista
HipóteseConflitos são rarosConflitos são prováveis ou caros
CoordenaçãoVerifica a versão na escritaBloqueia antes da alteração
Comportamento no conflitoRejeita ou repete a operaçãoFaz a operação concorrente esperar ou falhar
Custo dominanteRetries e recálculoEspera, contenção e manutenção do lock
Throughput com baixa contençãoTende a ser maiorPode introduzir espera desnecessária
Risco com alta contençãoMuitos retriesFilas de espera e deadlocks
Melhor encaixeLeituras frequentes e escritas rarasRecursos muito disputados e transações curtas

Essa regra orienta a investigação, mas não decide sozinha. Meça o comportamento real e considere a semântica da operação.

Exemplo prático: o último item em estoque

O estoque é um bom exemplo porque permite mais de uma solução correta.

Opção 1: atualização atômica

Se a regra couber em uma única instrução, talvez não seja necessário ler antes nem manter uma coluna de versão:

UPDATE products
SET stock = stock - 1
WHERE id = 123
  AND stock > 0;

Se uma linha for atualizada, a reserva foi feita. Se zero linhas forem atualizadas, não havia estoque disponível quando o banco executou a operação.

Essa costuma ser a solução mais simples para o exemplo isolado. Ela mostra uma lição importante: antes de escolher entre dois padrões, verifique se uma operação atômica ou uma restrição do banco já resolve o requisito.

Opção 2: controle otimista

Use uma versão quando a decisão envolver um estado lido anteriormente e você quiser detectar que esse estado mudou. O sistema ganha concorrência, mas precisa definir uma política de retry e limitar novas tentativas.

Opção 3: controle pessimista

Use um lock explícito quando for necessário proteger uma sequência curta de leituras e escritas relacionadas. Mantenha chamadas de rede, interação com usuários e processamento demorado fora da transação.

Não segure um lock enquanto chama um gateway de pagamento. Uma alternativa é reservar o recurso rapidamente, confirmar a transação local e continuar o workflow com estados explícitos, expiração e operações idempotentes.

Vantagens e limitações

Lock otimista

Vantagens

  • Não mantém locks durante a fase de leitura ou trabalho;
  • Reduz espera quando conflitos são raros;
  • Funciona bem com edições longas e aplicações distribuídas;
  • Torna o conflito visível para a regra de negócio.

Limitações

  • Exige tratamento explícito quando a escrita afeta zero linhas;
  • Retries precisam ser limitados e seguros;
  • Muita contenção pode produzir trabalho descartado e piorar a carga;
  • Comparar apenas timestamps pode ser frágil se a precisão ou a origem do relógio não forem adequadas.

Lock pessimista

Vantagens

  • Serializa o acesso ao recurso protegido;
  • Evita trabalho especulativo que seria descartado;
  • Facilita raciocinar sobre sequências transacionais curtas;
  • Pode ser mais previsível quando o recurso é muito disputado.

Limitações

  • Aumenta latência para operações concorrentes;
  • Reduz throughput quando muitos fluxos esperam pelo mesmo recurso;
  • Transações longas ampliam a contenção;
  • Diferentes ordens de aquisição podem causar deadlocks.

Boas práticas

Mantenha locks pelo menor tempo possível

Faça dentro da transação somente o trabalho necessário para preservar a consistência. Não espere resposta humana e evite chamadas a serviços externos enquanto mantém locks.

Use a menor granularidade que preserve a regra

Quando o banco e o modelo permitirem, prefira proteger as linhas necessárias em vez de bloquear uma tabela inteira. Mesmo assim, confirme o plano de execução: uma consulta mal indexada pode bloquear ou examinar mais dados do que o esperado.

Defina uma política de retry

Conflitos otimistas, deadlocks e timeouts podem exigir nova tentativa. Use um limite de tentativas, backoff com jitter quando apropriado e idempotência para não duplicar efeitos.

Observe a contenção em produção

Monitore pelo menos:

  • Quantidade de conflitos e retries;
  • Tempo de espera por locks;
  • Timeouts e deadlocks;
  • Duração das transações;
  • Linhas afetadas pelas atualizações condicionais.

Sem essas métricas, uma estratégia pode parecer eficiente enquanto transfere o custo para espera ou repetição invisível.

Erros comuns

Confundir transação com ausência de concorrência

Uma transação define atomicidade e isolamento conforme a configuração do banco. Ela não significa automaticamente que nenhuma outra transação possa ler ou alterar os mesmos dados.

Ignorar o número de linhas afetadas

No modelo otimista, 0 rows affected é parte do protocolo. Tratar a consulta como sucesso sem conferir esse resultado reintroduz a atualização perdida.

Repetir operações que não são idempotentes

Um retry pode cobrar um cartão, publicar uma mensagem ou enviar um e-mail novamente. A tentativa precisa ser segura, ou seus efeitos externos devem possuir mecanismos próprios de deduplicação.

Manter locks durante chamadas externas

Um serviço lento ou indisponível prolonga a transação, aumenta a fila de espera e pode transformar uma falha externa em saturação do banco.

Adquirir locks em ordens diferentes

Se uma transação bloqueia A e depois B, enquanto outra bloqueia B e depois A, ambas podem ficar esperando. Adote uma ordem consistente e esteja preparado para tratar deadlocks abortados pelo banco.

Aplicar retry sem limite

Sob alta contenção, o controle otimista pode virar um ciclo de leitura, conflito e nova tentativa. Retries precisam de limite; às vezes é melhor rejeitar, enfileirar ou mudar a estratégia.

Como escolher?

Faça estas perguntas:

  1. Com que frequência duas operações alteram o mesmo registro?
  2. Quanto trabalho é perdido quando detectamos o conflito?
  3. A operação pode ser repetida com segurança?
  4. Por quanto tempo a transação precisaria manter o lock?
  5. Uma atualização atômica ou restrição de unicidade resolve o problema de forma mais simples?
  6. O que deve acontecer quando o recurso está ocupado: esperar, falhar, tentar novamente ou enfileirar?
Conflito raro + retry barato
              ↓
       Controle otimista

Conflito frequente + retry caro + transação curta
              ↓
       Controle pessimista

Sistemas reais também podem combinar estratégias. Um catálogo pode usar versionamento nas edições administrativas, enquanto uma reserva de estoque disputado usa uma atualização atômica ou um lock pessimista em uma transação curta.

Perguntas de entrevista

  1. Como uma coluna de versão detecta uma atualização concorrente?
  2. Por que SELECT ... FOR UPDATE precisa estar dentro da mesma transação que a atualização?
  3. Em quais condições retries otimistas podem reduzir o throughput do sistema?
  4. Como você evitaria um deadlock ao atualizar dois recursos?
  5. Para reservar o último item em estoque, quando uma atualização atômica seria suficiente?
  6. Como locks de banco, idempotência e unique constraint se complementam em um fluxo de pagamento?

Próximos conteúdos

Para aprofundar o tema, estude transações e ACID, níveis de isolamento, idempotência e deadlocks. Eles explicam quais garantias o banco oferece, como retries afetam efeitos externos e por que o mesmo SQL pode se comportar de maneira diferente conforme o banco e a configuração.