Uma transferência de R$ 500 parece exigir apenas duas alterações: retirar o valor de uma conta e adicioná-lo a outra. Mas o que acontece se o servidor cair depois do débito e antes do crédito?

Conta de origem:  R$ 1.000 → R$ 500
                              ✕ falha
Conta de destino: R$   200 → não foi atualizada

Sem uma unidade que agrupe as duas operações, o dinheiro pode desaparecer. Transações existem para que alterações relacionadas sejam confirmadas ou desfeitas como um conjunto. As propriedades ACID descrevem as garantias esperadas dessa unidade.

O que é uma transação?

Uma transação é uma sequência de operações tratada pelo banco como uma única unidade lógica. Ela começa, executa leituras e escritas e termina de uma destas formas:

  • COMMIT: confirma as alterações;
  • ROLLBACK: cancela as alterações ainda não confirmadas.

Em SQL, o fluxo básico é:

BEGIN;

UPDATE accounts
SET balance = balance - 500
WHERE id = 101;

UPDATE accounts
SET balance = balance + 500
WHERE id = 202;

COMMIT;

Se uma operação falhar antes do COMMIT, a transação pode executar ROLLBACK e retornar ao estado anterior. Sem um bloco explícito, muitos bancos e clientes usam autocommit: cada instrução é uma transação independente. Nesse modo, confirmar o primeiro UPDATE não garante que o segundo também será executado.

Transação também não é sinônimo de requisição HTTP. Uma requisição pode abrir nenhuma, uma ou várias transações. Da mesma forma, manter uma transação aberta durante toda a requisição costuma ser desnecessário e pode aumentar contenção.

O que significa ACID?

ACID reúne quatro propriedades: Atomicidade, Consistência, Isolamento e Durabilidade. Elas respondem a tipos diferentes de falha.

PropriedadePergunta que respondeIdeia central
AtomicidadeE se uma etapa falhar?Tudo é confirmado ou tudo é desfeito
ConsistênciaO estado final respeita as regras?A transação leva o banco de um estado válido a outro
IsolamentoE se outras transações ocorrerem ao mesmo tempo?A concorrência não deve produzir resultados proibidos
DurabilidadeE se houver uma falha depois do commit?Uma confirmação não deve desaparecer

Atomicidade: tudo ou nada

A atomicidade impede que apenas parte da unidade lógica seja confirmada. Na transferência, débito, crédito e registro contábil precisam pertencer à mesma transação local.

Débito ✓ + Crédito ✓ + Registro ✓ → COMMIT
Débito ✓ + Crédito ✕              → ROLLBACK de tudo

“Atômica” não significa instantânea. A transação pode executar várias instruções e levar algum tempo; a garantia é que seu resultado confirmado não ficará pela metade.

Um SAVEPOINT permite desfazer uma parte do trabalho sem necessariamente cancelar toda a transação. Ele é útil em fluxos mais complexos, mas não transforma uma transação local em várias transações independentes.

Consistência: regras preservadas

Consistência significa que uma transação válida leva o banco de um estado que respeita suas invariantes para outro estado que também as respeita. Exemplos de invariantes são:

  • Uma chave primária não pode se repetir;
  • Uma chave estrangeira precisa apontar para um registro válido;
  • O saldo não pode ficar abaixo do limite permitido;
  • Um lançamento contábil precisa ter uma referência única;
  • A soma de débitos e créditos de uma operação precisa fechar.

O banco ajuda com PRIMARY KEY, FOREIGN KEY, UNIQUE, NOT NULL, CHECK, tipos e outras restrições. A aplicação complementa essas garantias com regras de domínio que não cabem em uma constraint simples.

A consistência do ACID também não é a mesma consistência do teorema CAP. Em ACID, o foco está nas invariantes da transação. Em CAP, o foco está no valor observado entre réplicas durante uma partição de rede.

Isolamento: concorrência controlada

Isolamento define quanto uma transação pode observar ou ser afetada por outras transações concorrentes. O ideal mais forte, chamado serializabilidade, faz o resultado das transações confirmadas ser equivalente a alguma execução uma após a outra.

Na prática, bancos oferecem níveis de isolamento com diferentes custos e garantias. Os nomes vêm do padrão SQL, mas detalhes de implementação variam entre bancos.

NívelProteção crescentePossíveis efeitos
Read UncommittedMínimaPode permitir leitura de dados ainda não confirmados
Read CommittedNão lê alterações sem commitDuas leituras podem retornar valores diferentes
Repeatable ReadMantém uma visão mais estávelAlgumas anomalias ainda dependem da implementação
SerializableEquivalente a alguma ordem serialPode abortar transações e exigir retry

Entre as anomalias que o isolamento tenta controlar estão:

  • Dirty read: ler uma alteração que outra transação ainda pode desfazer;
  • Non-repeatable read: ler o mesmo registro duas vezes e receber valores diferentes;
  • Phantom read: repetir uma consulta e encontrar um conjunto diferente de linhas;
  • Lost update: uma escrita sobrescrever uma alteração concorrente sem perceber;
  • Write skew: duas transações preservarem a regra individualmente, mas quebrarem a invariante quando confirmadas juntas.

Mais isolamento não é gratuito. Pode haver mais espera, aborts, retries ou trabalho de coordenação. Menos isolamento aumenta concorrência, mas transfere parte da proteção para locks explícitos, atualizações condicionais e desenho cuidadoso das regras.

Durabilidade: o commit permanece

Depois que o banco confirma o COMMIT, a durabilidade promete que o resultado sobreviverá às falhas cobertas pelo seu contrato, como a queda do processo ou do servidor.

Bancos relacionais normalmente usam um write-ahead log: antes de considerar a transação persistida, registram em armazenamento durável as informações necessárias para refazer suas alterações durante a recuperação.

Alterações → registro no log → persistência do log → COMMIT confirmado
                                      ↓
                            recuperação após falha

A garantia real depende da configuração, do armazenamento e da arquitetura. Modos de commit assíncrono podem aceitar uma pequena janela de perda em troca de menor latência.

Durabilidade também não substitui:

  • Backup, que permite recuperar exclusões acidentais, corrupção lógica e estados antigos;
  • Replicação, que mantém cópias para tolerar outras classes de falha;
  • Disaster recovery, que define como o serviço volta após a perda de uma máquina, zona ou região.

Uma transação durável pode preservar perfeitamente um DELETE executado por engano. Para voltar no tempo, ainda é necessário ter backup e um processo de restauração testado.

Exemplo prático: transferência entre contas

Considere uma transferência de R$ 500 da conta 101 para a conta 202. Além de mover o saldo, o sistema registra a operação em um ledger.

BEGIN;

SELECT id, balance
FROM accounts
WHERE id IN (101, 202)
ORDER BY id
FOR UPDATE;

UPDATE accounts
SET balance = balance - 500
WHERE id = 101
  AND balance >= 500;

-- A aplicação verifica se exatamente uma linha foi alterada.

UPDATE accounts
SET balance = balance + 500
WHERE id = 202;

INSERT INTO transfers (id, source_id, destination_id, amount)
VALUES ('trf-9f42', 101, 202, 500);

COMMIT;

Esse exemplo pressupõe que as duas contas existem, que o valor está na menor unidade monetária ou em um tipo decimal adequado e que transfers.id possui uma restrição UNIQUE ou é chave primária.

Cada parte contribui para uma garantia:

  • A transação desfaz todas as escritas se uma etapa falhar;
  • O UPDATE condicional impede o débito quando não há saldo suficiente;
  • A restrição única impede que a mesma transferência seja registrada duas vezes;
  • O FOR UPDATE coordena alterações concorrentes nas contas;
  • A ordenação dos locks reduz a chance de deadlock em transferências inversas;
  • O mecanismo de persistência do banco protege o resultado após o commit.

Ainda há trabalho para a aplicação. Se o débito alterar zero linhas, ela deve cancelar a transação e informar saldo insuficiente. Se ocorrer deadlock ou falha de serialização, pode ser necessário repetir toda a transação. O identificador único ajuda a tornar esse retry seguro.

E se houver um serviço externo?

Suponha que a operação também chame um gateway de pagamento ou publique uma mensagem. Uma transação do banco não engloba automaticamente esse efeito externo:

Banco local: COMMIT ✓
Publicação da mensagem: ✕

Manter locks enquanto espera uma API externa aumenta a duração da transação e não cria atomicidade entre os dois sistemas. Para coordenar banco e mensageria, padrões como transactional outbox registram a alteração de negócio e o evento na mesma transação local, publicando o evento depois com retry e idempotência.

Transações distribuídas com commit em duas fases existem, mas aumentam coordenação e possuem limitações operacionais. Em muitos sistemas, workflows explícitos, operações idempotentes e compensações são alternativas mais adequadas.

Quando utilizar transações?

Use uma transação quando duas ou mais operações formam uma única decisão de negócio e um estado parcial seria inválido ou difícil de reparar. Exemplos:

  • Mover saldo entre contas;
  • Criar um pedido e reservar seus itens;
  • Atualizar estoque e registrar sua movimentação;
  • Persistir uma entidade e o evento de outbox correspondente;
  • Alterar vários registros que compartilham uma invariante.

Uma única instrução SQL já é atômica no contexto do banco. Se a regra couber com segurança em um UPDATE condicional ou em uma constraint, essa solução costuma ser mais simples do que uma sequência de leitura e escrita.

Evite manter uma transação aberta enquanto o usuário preenche uma tela, um job executa processamento demorado ou uma API externa responde. Leia o necessário, faça o trabalho externo fora do bloco quando possível e deixe a seção transacional curta.

Vantagens

  • Preservam invariantes durante falhas parciais;
  • Simplificam o raciocínio sobre operações relacionadas;
  • Oferecem mecanismos para controlar concorrência;
  • Permitem cancelar trabalho ainda não confirmado;
  • Criam um ponto claro em que uma mudança passa a ser oficial.

Limitações e trade-offs

DecisãoBenefícioCusto ou risco
Aumentar o isolamentoEvita mais anomaliasMais aborts, retries ou coordenação
Manter a transação abertaAgrupa mais etapasLocks longos, contenção e consumo de recursos
Usar commit síncronoMaior garantia de durabilidadeLatência de persistência no caminho crítico
Coordenar vários sistemasFluxo de negócio mais abrangenteProtocolos, compensações e falhas mais complexos

Boas práticas

Mantenha a transação curta

Abra a transação o mais tarde possível e finalize-a assim que as operações relacionadas terminarem. Processamento pesado e chamadas de rede devem ficar fora dela sempre que a regra permitir.

Represente invariantes no banco

Use constraints e operações condicionais para proteger regras críticas. Uma validação feita apenas na aplicação pode sofrer uma corrida entre a leitura e a escrita.

Trate aborts como parte do protocolo

Deadlocks e falhas de serialização podem acontecer mesmo em um sistema correto. Faça rollback, repita a transação inteira quando for seguro e limite as tentativas.

Torne retries idempotentes

Use chaves de idempotência ou restrições únicas para que repetir uma solicitação não duplique cobranças, pedidos, transferências ou eventos.

Observe o comportamento em produção

Monitore duração das transações, espera por locks, deadlocks, falhas de serialização, taxa de rollback e latência de commit. Uma garantia correta no papel pode produzir contenção excessiva sob a carga real.

Erros comuns

Achar que BEGIN valida as regras de negócio

Atomicidade impede um resultado parcial, mas não reconhece sozinha que um saldo negativo ou uma reserva duplicada é inválida. Modele a invariante explicitamente.

Usar o nível de isolamento sem conhecer o banco

Os níveis possuem nomes padronizados, mas implementações podem oferecer garantias diferentes. Consulte a documentação do banco e teste os conflitos relevantes para sua regra.

Fazer “ler, validar e escrever” sem coordenar a concorrência

Duas transações podem ler o mesmo estado e aprovar a mesma decisão. Considere um UPDATE condicional, uma constraint, lock otimista, SELECT ... FOR UPDATE ou isolamento serializável.

Manter transações abertas durante chamadas externas

Uma API lenta prolonga locks e pode transformar uma indisponibilidade externa em saturação do banco. Além disso, rollback local não desfaz automaticamente o efeito remoto.

Repetir apenas a última instrução

Após um deadlock ou erro de serialização, as leituras que orientaram a decisão podem não ser mais válidas. Em geral, o retry precisa reexecutar a transação desde o início.

Confundir durabilidade com backup

Durabilidade protege o commit contra determinadas falhas. Backup protege a capacidade de recuperar dados após perda, corrupção ou erro humano. Um não substitui o outro.

Ignorar erros no commit

O sucesso de cada instrução não garante que o COMMIT será concluído. A aplicação só deve informar sucesso definitivo depois de confirmar o resultado da transação e precisa lidar com casos em que a conexão cai num momento ambíguo.

Perguntas de entrevista

  1. Qual é a diferença entre atomicidade e consistência em ACID?
  2. Por que uma transação não impede automaticamente uma atualização perdida?
  3. Como os níveis de isolamento equilibram corretude e concorrência?
  4. O que uma aplicação deve fazer após uma falha de serialização?
  5. Por que durabilidade não elimina a necessidade de backups?
  6. Como você implementaria uma transferência sem permitir saldo negativo?
  7. Por que uma chamada a um gateway de pagamento não deve permanecer dentro de uma transação longa?
  8. Como o transactional outbox reduz a inconsistência entre banco e mensageria?

Próximos conteúdos

Continue com lock otimista vs. pessimista para entender como coordenar escritas concorrentes. Depois, estude níveis de isolamento, deadlocks, transactional outbox e Saga Pattern para conectar transações locais a workflows distribuídos.

Também vale revisar o teorema CAP para não confundir a consistência das invariantes ACID com a consistência observada entre réplicas durante uma partição de rede.

Referências