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.
| Propriedade | Pergunta que responde | Ideia central |
|---|---|---|
| Atomicidade | E se uma etapa falhar? | Tudo é confirmado ou tudo é desfeito |
| Consistência | O estado final respeita as regras? | A transação leva o banco de um estado válido a outro |
| Isolamento | E se outras transações ocorrerem ao mesmo tempo? | A concorrência não deve produzir resultados proibidos |
| Durabilidade | E 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ível | Proteção crescente | Possíveis efeitos |
|---|---|---|
| Read Uncommitted | Mínima | Pode permitir leitura de dados ainda não confirmados |
| Read Committed | Não lê alterações sem commit | Duas leituras podem retornar valores diferentes |
| Repeatable Read | Mantém uma visão mais estável | Algumas anomalias ainda dependem da implementação |
| Serializable | Equivalente a alguma ordem serial | Pode 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
UPDATEcondicional 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 UPDATEcoordena 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ão | Benefício | Custo ou risco |
|---|---|---|
| Aumentar o isolamento | Evita mais anomalias | Mais aborts, retries ou coordenação |
| Manter a transação aberta | Agrupa mais etapas | Locks longos, contenção e consumo de recursos |
| Usar commit síncrono | Maior garantia de durabilidade | Latência de persistência no caminho crítico |
| Coordenar vários sistemas | Fluxo de negócio mais abrangente | Protocolos, 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
- Qual é a diferença entre atomicidade e consistência em ACID?
- Por que uma transação não impede automaticamente uma atualização perdida?
- Como os níveis de isolamento equilibram corretude e concorrência?
- O que uma aplicação deve fazer após uma falha de serialização?
- Por que durabilidade não elimina a necessidade de backups?
- Como você implementaria uma transferência sem permitir saldo negativo?
- Por que uma chamada a um gateway de pagamento não deve permanecer dentro de uma transação longa?
- 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.