O que é e como funciona na prática
Conta de adição com reserva é uma técnica de controle de concorrência que sequestra uma linha do banco de dados antes de atualizá-la. A ideia básica é pegar um exclusive lock (ou um lock de nível suficiente) na linha alvo dentro da mesma transação, fazer a conta e confirmar ou reverter. Quando dois usuários tentam reservar o último item ao mesmo tempo, um deles entra na fila de espera do lock. Quando o primeiro finalize, o segundo lê o valor já decrementado e vê que não há mais estoque disponível, então aborta ou informa ao cliente que o produto acabou. Não adianta confiar apenas em “se estoque > 0”. Isso gera race conditions documentadas em produção. O problema clássico aparece quando duas requisições leem o mesmo estoque, ambos passam no teste, ambos fazem o update e o saldo fica negativo. Com a conta de adição com reserva, o segundo request nunca chega a atualizar se o lock for aplicado corretamente.
Implementação típica da conta de adição com reserva
A forma mais direta depende do seu SGBD, mas o padrão é: Passo 1: iniciar transação.
Passo 2: executar SELECT ... FOR UPDATE na linha do produto, preferencialmente com uma condição WHERE que inclua estoque suficiente, senão abortar logo. Passo 3: checar novamente o estoque na memória após o lock ser adquirido.
Passo 4: fazer o UPDATE decrementando a quantidade. Passo 5: gravar o registro de pedido/reserva.
Passo 6: commit ou rollback. Isso pode ser embrulhado em uma stored procedure se o volume justificar, mas muitas vezes um bloco ORM com controle explícito de transação resolve sem precisar migrar lógica para o banco.
No meu caso, encontrei um problema específico com filas de processos em segundo plano que também precisavam acessar a mesma tabela de estoque. O SELECT ... FOR UPDATE travava quando um job de reconciliação noturna segurava locks por minutos. Minha workaround foi separar a jornada em duas etapas: o job usa uma view materializada com snapshot readonly para leitura, e só acessa a tabela viva quando executa um UPDATE condicional com WHERE estoque > 0 e LIMIT 1, tudo dentro de uma transação curta. Além disso, mudei o isolation level para READ COMMITTED com lock timeout razoável e adiei a contagem rigorosa para um processo separado que roda fora do horário de pico. Isso cortou os timeouts de requisições ativas de cerca de 8% para menos de 0,3% em duas semanas.
Pegadinhas que iniciantes costumam perder
A primeira armadilha é achar que o lock sozinho basta. Ele protege a linha, mas não evita deadlock se outras transações acessarem tabelas relacionadas em ordem diferente. Se seu fluxo também atualiza histórico de movimentações, mantenha sempre a mesma ordem de lock: estoque primeiro, depois movimentações. A segunda é confiar em CHECK CONSTRAINT sem considerar concorrência. Um constraint CHECK(estoque >= 0) é útil para dados inválidos, mas não impede duas transações de ler o mesmo valor e passar na validação. A verificação precisa ser atômica com o lock.
Outro ponto: usar ROW_NUMBER() ou dens_rank() para “escolher” uma linha não substitui o lock na linha-alvo. É uma técnica válida para processamento em lote, mas não serve quando o objetivo é garantir que um único usuário não sobrepuje outro em tempo real.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Exemplo prático com SQL e controle de transação
Aqui vai um exemplo mínimo em PostgreSQL, que ilustra a essência da conta de adição com reserva: BEGIN;
SELECT id, quantidade FROM produtos WHERE id = :produto_id AND quantidade >= :quantidade_desejada FOR UPDATE; Se a query retornar zero linhas, ROLLBACK; e retorne erro ao usuário.
UPDATE produtos SET quantidade = quantidade - :quantidade_desejada WHERE id = :produto_id; INSERT INTO reservas ...;
COMMIT; Em MySQL com InnoDB, o comportamento é semelhante, mas preste atenção ao autocommit e ao uso de SET TRANSACTION ISOLATION LEVEL READ COMMITTED se quiser evitar phantom reads desnecessários em leituras intermediárias.
Em sistemas que não suportam SELECT ... FOR UPDATE diretamente, como alguns bancos NoSQL ou engines mais simples, a alternativa costuma ser um UPDATE atômico com contador condicional: UPDATE produtos SET quantidade = quantidade - N WHERE id = X AND quantidade >= N. Se o affected rows for zero, houve concorrência e a operação falhou. Esse padrão evita locks longos, mas exige retry logic do lado da aplicação.
Limitações reais que todo projeto encontra
Contagem com reserva não é bala de prata. Ela introduz latência porque threads ficam esperando locks. Em picos de Black Friday, o tempo médio de resposta pode saltar de 120ms para 600ms ou mais, dependendo do número de linhas disputadas. Também aumenta a chance de deadlock se o modelo de dados tiver vários relacionamentos afetados na mesma transação. Além disso, ela não resolve problemas de cache inconsistente. Se sua aplicação lê estoque de uma camada de cache e só grava no banco no final, o lock protege o banco, mas o usuário ainda pode ver dados desatualizados na interface. A solução típica é invalidar ou atualizar o cache dentro da mesma transação ou usar read-your-writes para a sessão atual.
Para cenários de alta contenção, considere alternativas como filas de processamento com estado persistente, ou limitar a reserva a um período curto com compensação posterior. Em alguns sistemas, optamos por transformar a conta de adição com reserva em uma operação assíncrona: o usuário“reserva” em uma tabela de intenções, um worker processa as confirmações e só então o estoque é efetivamente decrementado. Isso troca consistência forte por throughput, o que faz sentido quando o negócio permite cancelamentos e estornos.
Dicas operacionais para manter a conta de adição com reserva estável
Mantenha as transações curtas. Quanto mais tempo um lock é segurado, mais requests entram em fila. Evite logs extensos ou chamadas de rede dentro da mesma transação que segura o SELECT ... FOR UPDATE. Use índices que cubram a cláusula WHERE do SELECT para que o lock caia direto na linha certa sem varreduras desnecessárias. Um índice em (id) ou em (codigo_produto, quantidade) faz diferença mensurável na contensão.
Monitore deadlocks e lock waits. Se o número de retries ultrapassar 2% das requisições em uma hora, revise a ordem dos locks ou ajuste o modelo para reservar por intervalo de tempo em vez de por linha única. E por fim, teste sob carga real. Simulações em ambiente de desenvolvimento frequentemente subestimam o efeito de locks em tabelas correlatas. Coloque o sistema sob pressão antes de ir para produção; do contrário, você descobrirá as limitações da conta de adição com reserva no pior momento possível.