O que é uma transação e por que ela existe?

Avatar de Arthur Guedon

Quando começamos a estudar bancos de dados, é comum encontrar uma definição direta para transação: um conjunto de operações executado como uma unidade lógica de trabalho. A definição está correta, mas não explica a parte mais interessante da história. Por que precisamos de uma unidade lógica de trabalho? Por que não executar cada INSERT, UPDATE ou DELETE individualmente e seguir adiante?

A resposta aparece quando deixamos de ver o banco de dados como uma coleção de tabelas e passamos a enxergá-lo como um sistema responsável por manter um estado consistente, enquanto muitas operações acontecem, algumas simultaneamente, e enquanto falhas podem ocorrer a qualquer momento. Uma operação de negócio costuma ser composta por várias operações menores, e o sistema não pode terminar em um estado em que apenas parte delas foi executada.

Atomicidade: ou tudo, ou nada

Imagine uma transferência bancária. Para o usuário, existe uma única ação: transferir R$ 1.000 de uma conta para outra. Para o banco de dados, são pelo menos duas modificações: retirar R$ 1.000 da primeira conta e adicioná-los à segunda. Se o primeiro UPDATE for executado e o servidor falhar antes do segundo, R$ 1.000 desaparecem do sistema. O problema não é que uma instrução SQL tenha falhado, e sim que uma operação que deveria ser indivisível foi parcialmente executada.

Daí surge a atomicidade, a primeira letra do acrônimo ACID. Ela não significa que o banco execute fisicamente tudo de uma vez. Significa que, para quem observa o estado confirmado do banco, nunca deve existir uma transação parcialmente aplicada: existe um antes e um depois. Enquanto a transação está em andamento, o banco pode modificar páginas em memória, registrar informações no log e realizar outras tarefas internas. Quando ela termina com COMMIT, seus efeitos passam a fazer parte do novo estado; quando termina com ROLLBACK, suas alterações deixam de fazer parte do estado da transação, por meio dos mecanismos de controle transacional do sistema.

Essa garantia não serve apenas para erros de programação. Falhas fazem parte da realidade de qualquer sistema: um processo pode ser encerrado, uma conexão pode cair, uma máquina pode perder energia, um disco pode falhar. Se cada operação fosse considerada definitiva no instante em que é executada, qualquer falha intermediária deixaria o banco em um estado que jamais deveria ter existido. O gerenciamento transacional introduz uma camada de controle entre a execução das operações e a confirmação de seus efeitos.

Essa camada se expressa em dois dos comandos mais importantes do SQL: COMMIT e ROLLBACK. O COMMIT informa ao banco que as alterações da transação devem ser efetivadas. O ROLLBACK abandona a transação e reverte seus efeitos. Na transferência, retiramos o valor da conta de origem, adicionamos à conta de destino e, somente se tudo for concluído corretamente, confirmamos.

BEGIN;
UPDATE contas
SET saldo = saldo - 1000
WHERE id = 1;
UPDATE contas
SET saldo = saldo + 1000
WHERE id = 2;
COMMIT;

O exemplo é simples, mas esconde uma ideia poderosa. O BEGIN estabelece uma fronteira transacional: tudo entre ele e o COMMIT forma uma unidade lógica de trabalho. Se algo impedir a conclusão, a aplicação emite ROLLBACK em vez de COMMIT, e o banco volta ao estado anterior. O banco ganha, assim, uma forma explícita de distinguir trabalho em andamento de estado confirmado.

Consistência: preservando as regras do modelo

Um banco de dados não trabalha apenas com operações individuais, mas com estados. Antes da transferência existe um estado das contas; depois, esperamos outro. O que importa não é apenas se cada UPDATE funcionou isoladamente, mas se o conjunto levou o sistema de um estado válido para outro estado válido.

Esse é o sentido de consistência no ACID: uma transação deve preservar as regras e invariantes que definem os estados válidos. Na transferência, um invariante natural é que a soma dos saldos não muda, porque o dinheiro só trocou de conta. Outro é que nenhuma conta pode ficar com saldo negativo, o que podemos declarar diretamente no banco:

ALTER TABLE contas
ADD CONSTRAINT saldo_nao_negativo CHECK (saldo >= 0);

Com essa restrição, uma transferência que deixaria a conta de origem negativa é rejeitada, e a transação inteira pode ser revertida. O mesmo vale para chaves, unicidade e integridade referencial.

Vale notar que consistência não é algo que o banco inventa sozinho. As restrições declaradas no modelo são impostas pelo próprio banco, mas regras de negócio mais complexas dependem de como a aplicação e o esquema foram projetados. A transação não define o que é um estado válido; ela garante que, se o banco parte de um estado válido, os efeitos confirmados não o deixem em um estado inválido.

Isolamento: quando duas transações acontecem ao mesmo tempo

Há um problema ainda maior: e se duas transações ocorrerem simultaneamente?

Imagine duas pessoas comprando ao mesmo tempo o último produto em estoque. Ambas consultam o banco e encontram quantidade = 1. Ambas concluem que há um produto disponível e registram a venda. Sem nenhum controle de concorrência, podemos terminar com duas vendas para um único item. Aqui não houve falha no meio de nenhuma transação: as duas podem ter sido executadas por completo. O problema é que uma interferiu na outra, pois cada uma tomou uma decisão com base em uma leitura que deixou de ser verdadeira. Esse padrão é conhecido como lost update.

O isolamento é a tentativa de controlar como transações concorrentes enxergam e afetam o estado do banco. Mas ele não é uma chave que se liga ou desliga. Existe um equilíbrio entre a consistência da visão dos dados, a concorrência e o desempenho, e por isso os bancos oferecem diferentes níveis de isolamento, como Read Committed, Repeatable Read e Serializable. Cada nível protege contra um conjunto diferente de anomalias: leituras de dados ainda não confirmados (dirty reads), leituras que mudam dentro da mesma transação (non-repeatable reads), linhas que aparecem ou somem entre consultas (phantoms), entre outras.

Isso significa que escolher um nível não resolve, por si só, todos os problemas. No caso do estoque, o Read Committed impede que uma transação leia dados não confirmados, mas não impede que duas transações leiam o mesmo valor e ambas o decrementem. Para esse cenário, é preciso um nível mais forte (como Serializable, ou Repeatable Read em bancos que detectam a atualização concorrente e abortam uma das transações, exigindo nova tentativa) ou um bloqueio explícito da linha, como SELECT ... FOR UPDATE. Também é possível reescrever a operação para que a verificação e a escrita aconteçam de forma atômica:

UPDATE estoque
SET quantidade = quantidade - 1
WHERE produto_id = 10 AND quantidade > 0;

Se nenhuma linha for afetada, o produto acabou. Os detalhes de cada nível variam entre bancos, e vale consultar a documentação do sistema que você usa.

Durabilidade: o que foi confirmado não se perde

Falta uma última questão. Suponha que tudo tenha funcionado: a transferência foi executada, a transação recebeu COMMIT e a aplicação informou ao usuário que a operação foi concluída. Um segundo depois, o servidor perde energia. Quando a máquina voltar, o banco precisa continuar sabendo que aquela transação foi confirmada. Não seria aceitável dizer: “a operação foi confirmada, mas perdi a informação”.

Essa é a durabilidade: uma vez confirmada, a transação tem seus efeitos preservados diante das falhas que o sistema foi projetado para suportar. É aqui que a arquitetura interna dos bancos fica fascinante. Sistemas como o PostgreSQL utilizam o Write-Ahead Logging (WAL), conceito que já discutimos em outro artigo aqui. As mudanças são registradas no log de transações antes de serem gravadas definitivamente nos arquivos de dados, e em configurações de commit síncrono, o COMMIT só é considerado concluído depois que o registro correspondente do WAL foi enviado ao armazenamento durável. Se o sistema falhar, o processo de recuperação usa esse log para reconstruir o estado necessário. A transação não é apenas uma abstração do SQL: está profundamente conectada aos mecanismos físicos que permitem ao banco sobreviver a falhas.

Juntando as peças

Quando colocamos as quatro propriedades lado a lado, o ACID deixa de ser um acrônimo para decorar em provas e entrevistas. A atomicidade responde ao problema da execução parcial. A consistência trata da preservação das regras que definem estados válidos. O isolamento lida com a interferência entre operações concorrentes. A durabilidade garante que o que foi confirmado sobreviva às falhas previstas. Cada propriedade responde a uma classe diferente de problema, e a transação é o mecanismo que reúne essas garantias em torno de uma unidade lógica de trabalho.

Por isso, talvez a melhor forma de compreender transações seja deixar de vê-las como uma funcionalidade do SQL e enxergá-las como uma resposta de engenharia a um problema inevitável: o mundo real não executa operações perfeitamente. Sistemas falham, usuários disputam os mesmos recursos, processos são interrompidos e operações complexas se compõem de muitas operações menores.

Essa perspectiva também explica por que as transações são tão fundamentais na Engenharia de Dados. Em um pipeline, em uma aplicação financeira ou em um simples cadastro, estamos sempre lidando com mudanças de estado, e o desafio não é apenas escrever dados, mas garantir que o sistema continue correto quando essas escritas acontecem em condições imperfeitas. COMMIT, ROLLBACK, isolamento, locks, MVCC, WAL e recovery não são assuntos independentes, são partes de uma mesma resposta a uma pergunta simples e profundamente importante: como fazer um sistema continuar confiável quando as coisas inevitavelmente dão errado?


Deixe um comentário