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_id | stock | version |
|---|---|---|
| 123 | 1 | 7 |
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ério | Otimista | Pessimista |
|---|---|---|
| Hipótese | Conflitos são raros | Conflitos são prováveis ou caros |
| Coordenação | Verifica a versão na escrita | Bloqueia antes da alteração |
| Comportamento no conflito | Rejeita ou repete a operação | Faz a operação concorrente esperar ou falhar |
| Custo dominante | Retries e recálculo | Espera, contenção e manutenção do lock |
| Throughput com baixa contenção | Tende a ser maior | Pode introduzir espera desnecessária |
| Risco com alta contenção | Muitos retries | Filas de espera e deadlocks |
| Melhor encaixe | Leituras frequentes e escritas raras | Recursos 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:
- Com que frequência duas operações alteram o mesmo registro?
- Quanto trabalho é perdido quando detectamos o conflito?
- A operação pode ser repetida com segurança?
- Por quanto tempo a transação precisaria manter o lock?
- Uma atualização atômica ou restrição de unicidade resolve o problema de forma mais simples?
- 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
- Como uma coluna de versão detecta uma atualização concorrente?
- Por que
SELECT ... FOR UPDATEprecisa estar dentro da mesma transação que a atualização? - Em quais condições retries otimistas podem reduzir o throughput do sistema?
- Como você evitaria um deadlock ao atualizar dois recursos?
- Para reservar o último item em estoque, quando uma atualização atômica seria suficiente?
- 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.