Pular para o conteúdo
9 min de leitura

Níveis de isolamento: os fenômenos da concorrência no banco

Por Equipe Tech do Sonne ·

Dirty read, non-repeatable read e phantom read explicados, e como READ COMMITTED, REPEATABLE READ e SERIALIZABLE trocam garantia por desempenho.

Neste artigo

Quando uma única transação roda sozinha em um banco, a vida é simples: ela lê, escreve, confirma, e ninguém interfere. O problema aparece quando dezenas ou centenas de transações rodam ao mesmo tempo, disputando as mesmas linhas. Duas pessoas comprando o último ingresso, um relatório somando saldos enquanto transferências acontecem, um contador sendo incrementado por vários processos. Sem regras claras, a concorrência produz resultados que não correspondem a nenhuma execução sequencial possível — e o dado corrompe de formas difíceis de reproduzir.

O isolamento, o "I" do ACID, é a garantia que controla essa interferência. Mas isolamento não é um interruptor de liga-desliga. É um botão de intensidade. Os bancos oferecem vários níveis de isolamento, cada um permitindo mais ou menos que as transações enxerguem os efeitos umas das outras. Níveis mais fortes eliminam mais anomalias, mas custam mais desempenho e geram mais conflitos; níveis mais fracos deixam o banco mais rápido e mais paralelo, ao preço de permitir certos fenômenos estranhos.

Para escolher o nível certo você precisa conhecer os fenômenos que cada nível permite ou proíbe. O padrão SQL define três anomalias clássicas de leitura — leitura suja, leitura não-repetível e leitura fantasma — e organiza os níveis em torno de quais delas são impedidas. Vamos entender cada fenômeno com um exemplo concreto e depois mapear os níveis.

Os três fenômenos clássicos#

Leitura suja (dirty read)#

Uma leitura suja acontece quando uma transação lê um dado que outra transação alterou mas ainda não confirmou. Você está lendo algo que pode nunca ter existido de verdade.

Imagine que a transação A debita cem reais de uma conta e ainda não deu COMMIT. Nesse instante, a transação B lê o saldo e vê o valor já debitado. B toma uma decisão baseada nesse número. Então A encontra um erro e faz ROLLBACK — o débito nunca aconteceu. Mas B já agiu sobre um valor fantasma que foi desfeito. B leu "sujeira": um estado intermediário e provisório de outra transação.

A leitura suja é a anomalia mais perigosa e mais fácil de proibir. Praticamente todos os bancos sérios a impedem por padrão, porque agir sobre dados não confirmados é quase sempre um erro. Basear-se em algo que pode ser desfeito é construir sobre areia.

Leitura não-repetível (non-repeatable read)#

Uma leitura não-repetível acontece quando uma transação lê a mesma linha duas vezes e obtém valores diferentes, porque outra transação confirmou uma alteração entre as duas leituras.

Suponha que a transação A leia o saldo de uma conta e veja duzentos reais. Ela faz algum processamento e, mais adiante, lê o mesmo saldo de novo. Nesse meio-tempo, a transação B transferiu cinquenta reais e deu COMMIT. Agora A lê cento e cinquenta. Dentro da mesma transação, a mesma pergunta recebeu duas respostas diferentes. Isso quebra qualquer lógica que assuma estabilidade dos dados ao longo da transação — por exemplo, um cálculo que soma valores em várias etapas contando com que a base não mude embaixo dos pés.

A diferença para a leitura suja é sutil mas fundamental: aqui o dado que B alterou foi confirmado. Não é sujeira; é uma mudança real e permanente. O problema não é ter lido algo inválido, e sim a instabilidade dentro da própria transação de A.

Leitura fantasma (phantom read)#

Uma leitura fantasma acontece quando uma transação executa a mesma consulta com filtro duas vezes e o conjunto de linhas retornado muda, porque outra transação inseriu ou removeu linhas que satisfazem o filtro.

O fenômeno é parente da leitura não-repetível, mas atinge o conjunto de linhas, não o valor de uma linha existente. Imagine que a transação A conte quantos pedidos existem acima de mil reais e obtenha dez. Depois, na mesma transação, ela roda a mesma contagem e obtém onze, porque a transação B inseriu um novo pedido de mil e duzentos e confirmou. O décimo primeiro pedido é o "fantasma": ele apareceu do nada entre duas execuções da mesma consulta.

A distinção importa porque impedir leitura não-repetível — que trava as linhas lidas — não basta para impedir fantasmas. A linha nova não existia para ser travada quando A leu pela primeira vez. Impedir fantasmas exige travar não só as linhas existentes, mas a faixa de valores que o filtro cobre, um mecanismo mais caro.

Os níveis de isolamento#

O padrão SQL define quatro níveis, cada um proibindo um subconjunto progressivamente maior das anomalias.

READ UNCOMMITTED#

O nível mais fraco. Permite tudo, inclusive leitura suja. Uma transação pode enxergar alterações não confirmadas de outra. Na prática, é raramente útil e raramente o padrão; alguns bancos nem o implementam de verdade, tratando-o como um nível mais forte. A não ser que você tenha um caso muito específico e tolerante a lixo, esse nível é para ser evitado.

READ COMMITTED#

Este nível garante que você só lê dados confirmados — nada de leitura suja — mas não garante estabilidade entre leituras. Cada comando dentro da transação enxerga um retrato dos dados no momento em que aquele comando começou. Como consequência, leitura não-repetível e leitura fantasma continuam possíveis: dois SELECT na mesma transação podem ver valores e conjuntos diferentes se outra transação confirmou algo no intervalo.

É o padrão de muitos bancos, incluindo o PostgreSQL, por ser um equilíbrio pragmático. Ele elimina a anomalia mais perigosa (ler sujeira) e mantém alta concorrência, ao custo de exigir que o desenvolvedor não assuma que os dados permanecem imóveis ao longo de uma transação. Para a maioria das operações — inserir um pedido, atualizar um perfil — isso é perfeitamente adequado.

REPEATABLE READ#

Este nível garante que, uma vez que a transação leu um dado, ele permanecerá igual até o fim da transação. Elimina a leitura não-repetível: a transação enxerga um retrato consistente do banco tirado no seu início, e toda leitura reflete esse retrato, ignorando alterações confirmadas por outras transações depois desse ponto.

Sob a definição estrita do padrão SQL, REPEATABLE READ ainda permitiria leituras fantasma. Na prática, a implementação de vários bancos modernos, que usam versionamento de linhas (MVCC), acaba entregando um nível mais forte que já barra boa parte dos fantasmas em consultas de leitura, embora certas combinações de leitura e escrita ainda possam produzir anomalias de escrita concorrente. O ponto importante é que aqui você ganha estabilidade: um relatório longo que soma e recruza dados vê uma base congelada, sem sustos de valores mudando no meio.

O custo é o aumento de conflitos. Como a transação insiste em ver o retrato antigo, se ela tentar escrever sobre uma linha que outra transação já modificou e confirmou, o banco pode abortar a transação com um erro de serialização, exigindo retry. Trabalhar nesse nível significa preparar o código para reexecutar transações que abortam por conflito.

SERIALIZABLE#

O nível mais forte garante que o resultado de transações concorrentes é idêntico a alguma execução sequencial delas, uma após a outra. Todas as três anomalias são impedidas, inclusive fantasmas. É como se, do ponto de vista lógico, nunca houvesse concorrência de verdade.

Esse é o nível que você quer quando a corretude é inegociável e a lógica envolve decisões baseadas em conjuntos de linhas que não podem mudar — verificar se ainda há vaga antes de reservar, garantir que uma regra global sobre várias linhas seja mantida. O banco alcança isso ou serializando o acesso com travas mais abrangentes, ou detectando conflitos de serialização e abortando transações que violariam a equivalência sequencial.

O preço é o mais alto: menor paralelismo e mais transações abortadas que precisam ser repetidas. Rodar todo o sistema em SERIALIZABLE costuma criar gargalos desnecessários. A abordagem sensata é usar o nível padrão para a maioria das operações e elevar para SERIALIZABLE cirurgicamente, apenas nas transações cuja corretude depende de impedir todas as anomalias.

Escolhendo o nível na prática#

A pergunta a se fazer para cada operação crítica é: minha lógica assume que os dados que li permanecem estáveis durante a transação, ou que o conjunto de linhas de uma condição não muda? Se a resposta é não — você lê, decide e escreve rapidamente sobre linhas independentes — READ COMMITTED costuma bastar. Se a resposta é sim, você precisa de REPEATABLE READ ou SERIALIZABLE.

Há também uma alternativa que dispensa subir o nível global: o bloqueio explícito. Em vez de elevar toda a transação, você pode travar as linhas relevantes na leitura com uma cláusula como SELECT ... FOR UPDATE. Isso diz ao banco "vou modificar estas linhas, segure-as para mim", impedindo que outra transação as altere até você confirmar. É a ferramenta certa para o caso clássico de ler um saldo, decidir com base nele e atualizá-lo: travar a linha na leitura elimina a corrida entre os dois caixas eletrônicos sem o custo de serializar o banco inteiro.

Independentemente do nível, uma regra atravessa tudo: transações em níveis mais fortes vão abortar por conflito de serialização, e isso é normal, não é bug. O banco está protegendo a corretude ao recusar uma execução que violaria as garantias. O código precisa detectar esse erro específico e reexecutar a transação. Uma transação idempotente, preparada para o retry, transforma o conflito em um contratempo invisível ao usuário.

O erro de assumir isolamento que você não pediu#

Muitos bugs de concorrência nascem de uma suposição implícita: o desenvolvedor escreve uma lógica de ler-decidir-escrever contando com estabilidade que o nível padrão READ COMMITTED não oferece. O código funciona em todos os testes, porque testes raramente reproduzem a corrida exata, e quebra em produção sob carga, quando duas requisições atingem a mesma linha no milissegundo errado. Conhecer os fenômenos permite reconhecer esse padrão perigoso antes que ele chegue à produção — e escolher, conscientemente, o nível ou o bloqueio que fecha a janela.

Fechando o raciocínio#

Níveis de isolamento são um mostruário de trade-offs entre corretude e desempenho. Do lado fraco, READ COMMITTED entrega concorrência alta e barra a leitura suja, mas deixa os dados escorregadios entre leituras. Do lado forte, SERIALIZABLE entrega a ilusão de execução sequencial perfeita, ao custo de paralelismo e retries.

O engenheiro competente não escolhe um nível para o sistema inteiro por hábito; ele identifica onde a lógica de negócio depende de qual garantia e ajusta o botão por operação, complementando com bloqueio explícito quando faz sentido. Conhecer leitura suja, leitura não-repetível e leitura fantasma dá a você o vocabulário para prever exatamente o que pode dar errado sob concorrência — e essa previsão é o que separa um sistema que resiste à carga de um que corrompe dados de forma intermitente e inexplicável.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly