DadosAvançado10 min de leitura8 seções1992, Data aproximada

Níveis de isolamento

Transações concorrentes se atrapalham de formas específicas e nomeadas. O nível de isolamento é a escolha de quais dessas anomalias o banco vai impedir — e cada degrau a mais custa concorrência, ou custa transações abortadas.

Em 10 segundos

O banco não isola transações totalmente por padrão — ele oferece níveis, e cada um impede um conjunto nomeado de anomalias. Subir o nível não é grátis: ou reduz concorrência, ou passa a abortar transações, o que exige retry. E várias anomalias somem sozinhas se você parar de ler-para-depois-escrever.

Antes de ter nome

Gray já descrevia 'graus de consistência' em 1976; o SQL-92 os batizou pelas anomalias que proíbem — e cada banco depois implementou o nome à sua maneira.

  • #transacao
  • #concorrencia
  • #acid
  • #anomalia
  • #banco
Analogia

Duas pessoas editando a mesma planilha impressa

Cada uma tira uma cópia, escreve suas alterações e devolve. Quem devolve por último apaga o trabalho de quem devolveu antes — e ninguém percebe, porque a folha final está coerente. Nível de isolamento é o combinado sobre isso: da cópia livre (rápida, perde trabalho) até a caneta única passada de mão em mão (lenta, não perde nada).

01Conceito

As anomalias, uma a uma

Dirty read: você lê um valor que outra transação escreveu e ainda vai desfazer. Non-repeatable read: você lê a mesma linha duas vezes e o valor mudou no meio. Phantom: você roda a mesma consulta duas vezes e apareceram linhas novas.

Lost update: duas transações leem o mesmo valor, cada uma calcula em cima dele, e a segunda escrita apaga a primeira. Write skew: cada transação vê um estado válido, mas o efeito conjunto delas viola a regra.

As duas últimas são as caras. Lost update é o clássico do saldo: T1 lê 100 e vai debitar 30; T2 lê 100 e vai debitar 50. As duas escrevem, e o resultado é 50 — o débito de 30 sumiu sem erro, sem log, sem nada. Se isso for dinheiro, alguém pagou a conta.

Write skew é o mais sutil e o único que exige SERIALIZABLE. A regra é 'sempre pelo menos um médico de plantão'. Duas transações leem que há dois de plantão, cada uma conclui que pode liberar o seu, e as duas liberam. Nenhuma leu nada desatualizado; nenhuma escreveu na linha da outra. Mesmo assim, a regra foi violada — porque a invariante é sobre o conjunto, e nenhuma das duas o observou inteiro.

Vale um aviso sobre nomenclatura: o padrão SQL define quatro níveis pelas anomalias que eles proíbem, mas cada banco implementa à sua maneira. O REPEATABLE READ do Postgres impede phantoms (porque usa *snapshot isolation*), enquanto o padrão só exige isso em SERIALIZABLE. O SERIALIZABLE do Postgres é *serializable snapshot isolation*, que aborta em vez de bloquear. O nome do nível não define o comportamento — a implementação define.

Como se encaixa
  1. T1 lê saldo = 100
  2. as duas leem antes de qualquer escritaT2 lê saldo = 100
  3. T1 debita 30T1 escreve 70
  4. T2 debita 50 sobre o valor velhoT2 escreve 50
  5. o débito de 30 desapareceuSaldo final: 50
Lost update: nenhuma das duas transações fez nada de errado isoladamente, e nenhuma recebeu erro. O dinheiro simplesmente sumiu entre a leitura e a escrita.
02Conceito

A saída raramente é subir o nível

Diante de uma anomalia, o reflexo é escolher um nível mais forte. Quase sempre há uma saída melhor: eliminar a janela entre ler e escrever.

UPDATE contas SET saldo = saldo - 30 WHERE id = 1 AND saldo >= 30 não tem janela — o banco resolve a leitura e a escrita numa operação só, e nenhum nível de isolamento precisa entrar na conversa.

Quando a operação não cabe numa escrita só, as opções em ordem de custo são: bloqueio explícito (SELECT ... FOR UPDATE), que é pessimista e serializa só as linhas em disputa; versão otimista (WHERE versao = $atual), que não bloqueia nada e falha na hora do commit; e por fim SERIALIZABLE, que delega ao banco a detecção e o aborto.

SERIALIZABLE tem uma pegadinha que derruba sistemas: ele não faz esperar, ele aborta. O Postgres devolve 40001 e espera que você repita. Código que trata isso como erro genérico transforma o nível mais forte de isolamento numa fonte nova de erro 500 — o oposto do que se queria.

E há o custo de concorrência. Cada degrau reduz o quanto o banco pode executar em paralelo, e sob carga isso aparece como fila e latência. Não existe nível 'seguro por padrão': existe a invariante que você precisa preservar, e o mecanismo mais barato que a preserva.

03No mundo real

Como isso aparece na prática

O cupom de uso único usado três vezes

Cenário

Um cupom promocional tem limite de mil resgates. O código lê a contagem, compara com o limite e insere o resgate. Numa promoção com tráfego alto, o cupom foi resgatado 1.043 vezes.

Como o padrão entra

A verificação e a inserção passaram a acontecer numa operação só, com restrição no banco garantindo o limite — em vez de um SELECT seguido de INSERT.

Trade-off: A regra migra do código da aplicação para uma restrição no schema, o que a torna menos visível para quem lê só o serviço. Em troca, ela passa a ser impossível de violar por concorrência.

O SERIALIZABLE que virou erro 500

Cenário

Depois de um incidente de saldo inconsistente, um time subiu todas as transações críticas para SERIALIZABLE. A inconsistência sumiu — e a taxa de erro 500 subiu em horário de pico.

Como o padrão entra

As transações passaram a repetir automaticamente em caso de 40001, com backoff e jitter, e apenas as operações com invariante de conjunto ficaram no nível mais alto.

Trade-off: A latência do p99 piorou nas operações que agora repetem, e o código ficou mais complexo. É o preço de uma garantia que antes não existia.

SERIALIZABLE sem retry

Compila, roda, passa no code review. E quebra depois.

O nível mais forte foi escolhido para acabar com as inconsistências. Ninguém avisou que ele não bloqueia — ele aborta —, e o código não sabe repetir. O resultado é que a garantia mais cara do banco virou uma fonte nova de erro para o usuário.

// "Colocamos SERIALIZABLE, agora esta seguro."
await db.transacao({ isolamento: "SERIALIZABLE" }, async (tx) => {
  const saldo = await tx.saldo(contaId);
  await tx.debitar(contaId, valor);
});

// O que ninguem contou: em SERIALIZABLE o banco NAO faz voce esperar.
// Ele deixa as duas transacoes correrem e ABORTA uma no commit,
// com erro de serializacao (Postgres: 40001).
//
// Sem retry, esse erro chega ao usuario como "erro inesperado".
// E o codigo acima nao tem retry — entao o nivel mais forte
// deixou o sistema MENOS confiavel do que estava.

O que quebra

  • Sob concorrênciaO banco devolve 40001 e a transação é abortada; sem repetição, isso chega ao usuário como erro inesperado.
  • Em horário de picoQuanto mais tráfego, mais conflitos de serialização — a taxa de erro cresce exatamente quando o sistema é mais usado.
  • No diagnósticoO erro aponta para o banco, e não para a decisão de isolamento, o que faz o time procurar no lugar errado.

A saída: SERIALIZABLE é um contrato de duas partes: o banco detecta o conflito, e você repete. Trate 40001 como falha transitória, com backoff e jitter. E antes disso, verifique se a operação não caberia num UPDATE condicional — que não precisa de nível nenhum.

05Cuidado

Onde isto dá errado

  1. Assumir que o nível padrão é seguro

    Postgres e Oracle vêm em READ COMMITTED, que permite lost update tranquilamente. A maioria dos sistemas roda no padrão sem que ninguém tenha decidido isso — o padrão é uma escolha de quem instalou, não uma garantia.

  2. Confiar no nome do nível entre bancos diferentes

    REPEATABLE READ do Postgres impede phantoms; o do padrão SQL não. SERIALIZABLE do Postgres aborta; o de outros bancos bloqueia. Portar código entre bancos mantendo o nome do nível não mantém o comportamento.

  3. Ler, calcular e escrever dentro da transação

    Esse é o formato que produz lost update. Quase sempre há um UPDATE condicional equivalente que elimina a janela — e ele é mais barato que qualquer nível de isolamento.

  4. Achar que a transação protege o que está fora do banco

    Chamar uma API dentro da transação não desfaz a chamada no rollback. O isolamento cobre o banco, e só ele; efeito externo precisa de idempotência e compensação.

  5. Transação longa como jeito de garantir consistência

    Segurar a transação aberta enquanto se faz outro trabalho aumenta a janela de conflito, prende conexão e, em Postgres, atrasa o VACUUM. Transação é para ser curta.

  6. Ignorar write skew porque 'ninguém escreve na mesma linha'

    Write skew acontece justamente sem escrita concorrente na mesma linha: as transações escrevem em linhas diferentes e ainda assim violam uma regra sobre o conjunto. É o único caso que exige SERIALIZABLE de verdade.

06Interativo

Veja a anomalia acontecer

Mexa nos controles: a teoria acima vira comportamento observável.

Roteiro

Nível de isolamento

As duas leem 100, cada uma debita, e a segunda escrita apaga a primeira. Nenhuma recebe erro. Some a partir de SERIALIZABLE.

saldo commitado: 100
  1. T1ler
  2. T2ler
  3. T1escrever
  4. T1commit
  5. T2escrever
  6. T2commit
07Implementação

As anomalias e a saída atômica

// As quatro anomalias, e o nivel minimo que impede cada uma:
//
//   dirty read        (le o que nao commitou)     -> READ COMMITTED
//   non-repeatable    (le duas vezes, valor muda) -> REPEATABLE READ
//   phantom           (le duas vezes, LINHAS mudam) -> REPEATABLE READ*
//   lost update       (dois updates, um some)     -> REPEATABLE READ
//   write skew        (dois validos, juntos invalidos) -> SERIALIZABLE
//
// * no Postgres. No padrao SQL, phantom so cai em SERIALIZABLE.

// LOST UPDATE — o classico, e o mais caro:
// T1: le saldo (100) ... calcula 100-30 ... escreve 70
// T2:       le saldo (100) ... calcula 100-50 ... escreve 50
// Resultado: 50. O debito de 30 evaporou.

// A correcao nao e "subir o nivel": e nao ler para depois escrever.
await tx.query(
  "UPDATE contas SET saldo = saldo - $1 WHERE id = $2 AND saldo >= $1",
  [valor, contaId],
);
// O banco resolve a leitura e a escrita numa operacao so.
// Nenhuma janela entre uma coisa e outra — nenhuma anomalia possivel.
08Decisão

Quando usar × quando evitar

Use quando

3
  • Sempre que duas requisições podem tocar a mesma linha ao mesmo tempo.
  • Em toda operação que lê, calcula e escreve de volta.
  • Ao escolher entre nível, bloqueio explícito e escrita atômica.

Evite quando

3
  • Subir o nível sem saber qual anomalia se quer impedir.
  • SERIALIZABLE sem tratamento de 40001.
  • Transações longas seguradas por trabalho externo.

Testar o que ficou

6 armadilhas de Níveis de isolamento viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • O padrão do seu bancoPostgres e Oracle vêm em READ COMMITTED; MySQL/InnoDB vem em REPEATABLE READ. Quase ninguém verifica qual é.
  • O erro 40001 do Postgresserialization_failure é o banco dizendo que abortou a sua transação para preservar outra — e que você deveria repetir.
  • SELECT ... FOR UPDATEÉ como se pede bloqueio explícito quando o nível escolhido não garante o que a operação precisa.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Nao leia para depois escrever: peca ao banco fazer os dois.
UPDATE contas SET saldo = saldo - $1 WHERE id = $2 AND saldo >= $1

O que ele cobra

Nenhum salto a mais entre a chamada e o efeito.

  • Cada degrau a mais reduz concorrência ou aumenta transações abortadas
  • SERIALIZABLE obriga o código a saber repetir — sem retry, ele piora a confiabilidade
  • O comportamento real varia entre bancos com o mesmo nome de nível

Não vale se: a operação pode ser expressa como uma escrita atômica só. Um UPDATE condicional resolve mais barato que qualquer nível.

Onde isto aparece

Continue por aqui