DevGuideTK
← Trilha Backend

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écnicaComo funcionaResultado no teste
Ler e gravar sem proteçãoLê o valor, decide no código, gravaAs duas vendas foram confirmadas: o item foi vendido duas vezes
Gravação condicionalA própria gravação confere a condição: UPDATE ... WHERE estoque >= 1Uma transação atualizou a linha; a outra atualizou zero linhas
Trava pessimistaLê com SELECT ... FOR UPDATE, travando a linha até o fim da transaçãoA segunda transação esperou e, ao continuar, leu estoque 0
Isolamento mais forteRoda a transação em repeatable read ou serializableA 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çãoTécnica
Baixar estoque, debitar saldo, consumir um limiteGravação condicional
A decisão envolve cálculos ou dados de várias tabelas, numa operação curtaTrava pessimista (FOR UPDATE)
O usuário edita um registro por minutos antes de salvarTrava 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 linhaDividir 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çaEvite
Abrir a transação o mais tarde possível e confirmar logoAbrir 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ômicasChamar APIs externas, enviar e-mails ou publicar mensagens dentro da transação
Validar os dados de entrada antes de abrir a transaçãoEsperar uma resposta do usuário com a transação aberta
Acessar as tabelas sempre na mesma ordemTravar 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:

ProblemaPadrão
Gravar no banco e publicar uma mensagem, sem perder nenhuma das duasOutbox: a mensagem é gravada numa tabela na mesma transação e publicada depois por outro processo
Cobrar no serviço de pagamento e registrar o pedidoRegistrar 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çosSaga: uma sequência de transações locais com ações de compensação para desfazer

Resumo da decisão

  1. Toda operação com várias gravações relacionadas vai numa transação.
  2. 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.
  3. Regras importantes declaradas no banco, como última defesa.
  4. Transações curtas, sem chamadas externas, com travas sempre na mesma ordem.
  5. 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.