Transações e ACID
As garantias que impedem um sistema de ficar com dados pela metade quando algo falha, e de vender o mesmo item duas vezes.
Usar transações é o primeiro passo. As decisões estão em como impedir que transações simultâneas atrapalhem umas às outras e em até onde cada transação deve ir. Os comportamentos descritos aqui foram reproduzidos no PostgreSQL 17, com duas transações disputando o último item em estoque.
Quatro formas de tratar a concorrência
| Técnica | Como funciona | Resultado no teste |
|---|---|---|
| Ler e gravar sem proteção | Lê o valor, decide no código, grava | As duas vendas foram confirmadas: o item foi vendido duas vezes |
| Gravação condicional | A própria gravação confere a condição: UPDATE ... WHERE estoque >= 1 | Uma transação atualizou a linha; a outra atualizou zero linhas |
| Trava pessimista | Lê com SELECT ... FOR UPDATE, travando a linha até o fim da transação | A segunda transação esperou e, ao continuar, leu estoque 0 |
| Isolamento mais forte | Roda a transação em repeatable read ou serializable | A segunda transação falhou com erro de serialização |
Gravação condicional
A forma mais simples e mais eficiente. A conferência e a alteração acontecem na mesma instrução, que o banco executa de forma atômica:
UPDATE produtos
SET estoque = estoque - 1
WHERE id = 7
AND estoque >= 1;O código confere quantas linhas foram alteradas. Se foi uma, a venda pode seguir. Se foi zero, o estoque acabou, e a transação é desfeita. Não há trava explícita, nem espera longa, nem erro a tratar.
Serve quando a regra cabe numa condição sobre a própria linha que está sendo alterada.
Trava pessimista
Quando a decisão depende de uma lógica que não cabe num WHERE, como um cálculo com dados de outras tabelas, a linha é travada na leitura:
BEGIN;
SELECT estoque, preco FROM produtos WHERE id = 7 FOR UPDATE;
-- a aplicação decide com base nos valores lidos
UPDATE produtos SET estoque = estoque - 1 WHERE id = 7;
COMMIT;Qualquer outra transação que tente travar a mesma linha espera até o COMMIT ou o ROLLBACK. É uma solução correta, mas que transforma a disputa em fila: com muitos acessos à mesma linha, as transações passam o tempo esperando.
Trava otimista
Em vez de travar, cada linha tem um número de versão. A gravação só acontece se a versão ainda for a que foi lida:
UPDATE produtos
SET estoque = 0, versao = versao + 1
WHERE id = 7
AND versao = 3;Se outra transação alterou a linha antes, a versão mudou, e a gravação afeta zero linhas. A aplicação lê de novo e tenta outra vez, ou avisa o usuário. É a técnica certa quando a leitura e a gravação estão separadas por muito tempo, como um formulário que o usuário edita por minutos antes de salvar. Os ORMs costumam oferecer essa técnica pronta.
Isolamento mais forte
Em repeatable read ou serializable, o próprio banco detecta o conflito e faz uma das transações falhar com um erro de serialização (no PostgreSQL, o código 40001). O código precisa repetir a transação inteira quando isso acontece.
É a opção mais abrangente, porque protege também contra anomalias que as outras técnicas não cobrem, como regras que envolvem várias linhas. Em troca, toda transação passa a precisar de um laço de novas tentativas.
Como escolher a técnica
| Situação | Técnica |
|---|---|
| Baixar estoque, debitar saldo, consumir um limite | Gravação condicional |
| A decisão envolve cálculos ou dados de várias tabelas, numa operação curta | Trava pessimista (FOR UPDATE) |
| O usuário edita um registro por minutos antes de salvar | Trava otimista com versão |
| Regras que envolvem várias linhas, como "no máximo três reservas por sala no mesmo horário" | Serializable com novas tentativas |
| Contadores com escrita muito intensa na mesma linha | Dividir o contador em várias linhas ou agregar fora do banco |
O escopo da transação
Uma transação segura travas e conexões enquanto está aberta. Quanto mais tempo ela dura, mais as outras esperam.
| Faça | Evite |
|---|---|
| Abrir a transação o mais tarde possível e confirmar logo | Abrir a transação no início da requisição e confirmar no fim |
| Deixar dentro só as leituras e gravações que precisam ser atômicas | Chamar APIs externas, enviar e-mails ou publicar mensagens dentro da transação |
| Validar os dados de entrada antes de abrir a transação | Esperar uma resposta do usuário com a transação aberta |
| Acessar as tabelas sempre na mesma ordem | Travar linhas em ordens diferentes em lugares diferentes do código |
A última linha evita o impasse (deadlock): a transação A trava o produto 1 e espera o produto 2, enquanto a transação B travou o produto 2 e espera o produto 1. O banco detecta o impasse e cancela uma delas, que precisa ser repetida. Travar sempre na mesma ordem, como por id crescente, elimina a maior parte dos casos.
Transações e o resto do sistema
Uma transação não alcança sistemas externos. Os padrões mais comuns para lidar com isso:
| Problema | Padrão |
|---|---|
| Gravar no banco e publicar uma mensagem, sem perder nenhuma das duas | Outbox: a mensagem é gravada numa tabela na mesma transação e publicada depois por outro processo |
| Cobrar no serviço de pagamento e registrar o pedido | Registrar o pedido como pendente, cobrar fora da transação com chave de idempotência e confirmar depois |
| Uma operação que atravessa vários serviços | Saga: uma sequência de transações locais com ações de compensação para desfazer |
Resumo da decisão
- Toda operação com várias gravações relacionadas vai numa transação.
- Concorrência: gravação condicional quando a regra cabe no
WHERE; trava pessimista para operações curtas com lógica complexa; trava otimista para edições longas; serializable com novas tentativas para regras entre várias linhas. - Regras importantes declaradas no banco, como última defesa.
- Transações curtas, sem chamadas externas, com travas sempre na mesma ordem.
- Entre sistemas, outbox e sagas, e não transações.
O próximo nível mostra a compra com gravação condicional nas cinco linguagens e o tratamento de erros de serialização.
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Escolher a técnica de concorrência
Quero aprender a escolher entre gravação condicional, trava pessimista, trava otimista e isolamento serializable. Explique cada técnica com um exemplo de SQL, o que acontece quando duas transações disputam a mesma linha e o custo de cada uma. Depois me apresente estas situações, uma de cada vez, e pergunte qual técnica eu usaria: 1. Baixar o estoque de um produto na compra. 2. Um formulário de edição de cadastro que o usuário deixa aberto por dez minutos. 3. Reservar uma sala, com a regra de no máximo uma reserva por horário. 4. Transferir dinheiro entre duas contas. 5. Um contador de visualizações atualizado milhares de vezes por segundo. 6. Aprovar um pedido apenas se o total de pedidos aprovados do cliente no mês ficar abaixo de um limite. Corrija cada resposta e explique o que daria errado com as outras técnicas.
Desenhar o escopo das transações
Quero aprender a decidir o que fica dentro e o que fica fora de uma transação. Peça que eu descreva uma operação do meu sistema com várias etapas (por exemplo, finalizar um pedido com pagamento, baixa de estoque, e-mail de confirmação e publicação de evento). Se eu não tiver uma, use esse exemplo. Depois: 1. Separe as etapas que precisam ser atômicas das que não precisam. 2. Mostre por que chamar o serviço de pagamento ou enviar o e-mail dentro da transação é um problema. 3. Proponha a ordem correta das etapas, com o estado intermediário do pedido. 4. Explique como o padrão outbox garante que o evento seja publicado se e somente se a transação for confirmada. 5. Explique o que é um impasse (deadlock) e como a ordem de acesso às tabelas evita a maior parte deles. Termine com o desenho final da operação em passos numerados.
Restrições do banco como defesa
Quero entender por que vale a pena declarar regras de negócio no próprio banco de dados, e não só no código. Explique os tipos de restrição disponíveis (NOT NULL, UNIQUE, CHECK, chaves estrangeiras e, no PostgreSQL, restrições de exclusão) com um exemplo de regra de negócio para cada uma. Depois discuta: 1. O que acontece quando dois serviços, ou um script de correção, gravam na mesma tabela e só um deles conhece a regra. 2. Como uma restrição interage com uma transação quando é violada no meio dela. 3. Como transformar o erro da restrição numa mensagem útil para o usuário. 4. Quais regras não cabem numa restrição e precisam de outra solução. Pergunte quais regras existem no meu sistema e me ajude a decidir quais podem virar restrições.