DevGuideTK
← Trilha Backend

Níveis de isolamento

O que duas transações simultâneas enxergam uma da outra e as anomalias que cada nível permite.

O código desta página foi executado nas cinco linguagens contra um PostgreSQL 18. A matriz de anomalias do nível Conceito foi gerada por um script em Python que intercala duas conexões passo a passo, no PostgreSQL 18 e no MySQL 8.4.

A regra do plantão em serializable, em cada linguagem

A tabela do teste:

DROP TABLE IF EXISTS plantao;
CREATE TABLE plantao (medico text PRIMARY KEY, de_plantao boolean NOT NULL);
INSERT INTO plantao SELECT 'medico' || g, true FROM generate_series(1, 10) g;

Cada versão tem a mesma função: abre uma transação serializable, conta quem está de plantão, recusa se sobrar só um e, se não, tira o médico do plantão. Quando o banco recusa a transação com o erro 40001, a função espera um pouco e repete do começo, até dez vezes. O programa dispara dez pedidos ao mesmo tempo, um por médico.

import pg from "pg";

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 20 });
const espera = (ms: number) => new Promise((r) => setTimeout(r, ms));
let novasTentativas = 0;

// Regra: pelo menos um médico continua de plantão
async function sairDoPlantao(medico: string): Promise<"saiu" | "recusado"> {
  for (let tentativa = 1; ; tentativa++) {
    const conexao = await pool.connect();
    try {
      await conexao.query("BEGIN ISOLATION LEVEL SERIALIZABLE");
      const { rows } = await conexao.query(
        "SELECT count(*)::int AS n FROM plantao WHERE de_plantao");
      if (rows[0].n <= 1) {
        await conexao.query("ROLLBACK");
        return "recusado";
      }
      await conexao.query("UPDATE plantao SET de_plantao = false WHERE medico = $1", [medico]);
      await conexao.query("COMMIT");
      return "saiu";
    } catch (erro: any) {
      await conexao.query("ROLLBACK");
      // 40001: o banco desfez a transação por conflito; repetir é seguro
      if (erro.code !== "40001" || tentativa === 10) throw erro;
      novasTentativas++;
      await espera(Math.random() * 20 * tentativa); // espera crescente e aleatória
    } finally {
      conexao.release();
    }
  }
}

const medicos = Array.from({ length: 10 }, (_, i) => `medico${i + 1}`);
const resultados = await Promise.all(medicos.map(sairDoPlantao));
const { rows } = await pool.query("SELECT count(*)::int AS n FROM plantao WHERE de_plantao");
console.log({
  saiu: resultados.filter((r) => r === "saiu").length,
  recusado: resultados.filter((r) => r === "recusado").length,
  novasTentativas,
  continuamDePlantao: rows[0].n,
});
await pool.end();

O resultado, nas cinco linguagens

SaíramRecusadosNovas tentativasContinuam de plantão
TypeScript91121
Python91101
Go91201
Java91181
C#9112 e 13, em duas rodadas1
TypeScript em read committed10000

A última linha é a mesma função TypeScript, trocando só o nível para read committed: os dez médicos saíram, e a regra foi violada sem nenhum erro. O número de novas tentativas varia de uma execução para outra, porque depende da ordem em que as transações se cruzam. O resultado final não varia.

O que muda de uma linguagem para outra

Como pedir serializableComo reconhecer a recusa
TypeScriptBEGIN ISOLATION LEVEL SERIALIZABLEerro.code === "40001"
Pythonconexao.isolation_level = IsolationLevel.SERIALIZABLEerrors.SerializationFailure
Gopgx.TxOptions{IsoLevel: pgx.Serializable}*pgconn.PgError com Code == "40001"
JavasetTransactionIsolation(TRANSACTION_SERIALIZABLE)getSQLState() igual a "40001"
C#BeginTransactionAsync(IsolationLevel.Serializable)PostgresErrorCodes.SerializationFailure

A recusa pode chegar em qualquer comando da transação, inclusive no COMMIT. Por isso o laço de repetição envolve a transação inteira, e não um comando.

Em frameworks, a repetição costuma ser um decorador ou uma configuração: @Retryable no Spring, políticas de repetição do EF Core (EnableRetryOnFailure), ou uma função auxiliar como a das versões acima. O cuidado é o mesmo: repetir a transação inteira e só nos códigos de conflito.

Com MySQL

O mesmo código funciona no MySQL trocando os códigos: repita nos erros 1213 (impasse) e 1205 (espera por trava esgotada). No teste da matriz, o serializable do MySQL resolveu a escrita enviesada com um impasse: as duas transações travaram as linhas lidas, cada uma esperou a outra, e o banco escolheu uma para desfazer.

Lista de revisão

Nível

  • O time sabe qual é o nível padrão do banco em uso?
  • As operações que leem, calculam e gravam têm gravação condicional, FOR UPDATE ou versão?
  • As regras sobre várias linhas usam serializable, uma trava num registro comum ou uma restrição?

Repetição

  • A transação inteira é repetida, desde as leituras?
  • Só os códigos de conflito (40001 e 40P01 no PostgreSQL, 1213 e 1205 no MySQL) são repetidos?
  • Há limite de tentativas e espera crescente com variação aleatória?
  • O código não chama ROLLBACK numa transação que o driver já encerrou?

Escopo

  • As transações são curtas?
  • E-mails, filas e chamadas a outras APIs acontecem depois do COMMIT, e não dentro da transação?
  • Há um teste com transações simultâneas para cada regra sensível?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Reproduzir as anomalias no meu banco

    Quero reproduzir as anomalias de isolamento no banco que eu uso, para ver com meus olhos.
    
    Pergunte qual banco e versão eu uso.
    
    Me guie, com os comandos exatos para dois terminais:
    1. Subir o banco num container.
    2. Criar as tabelas de exemplo.
    3. Reproduzir cada anomalia em cada nível, dizendo o que digitar em cada terminal e em que ordem.
    4. Interpretar o resultado de cada passo.
    5. Montar uma tabela com o que eu observei.
  • Proteger uma regra com serializable

    Tenho uma regra de negócio que envolve várias linhas e quero protegê-la com o nível serializable.
    
    Pergunte qual é a regra, qual banco, linguagem e driver eu uso.
    
    Vamos em etapas:
    1. A função com a transação serializable.
    2. A repetição só nos códigos de conflito, com limite e espera aleatória.
    3. Um teste que dispara dezenas de pedidos simultâneos e confere que a regra se manteve.
    4. O mesmo teste em read committed, para provar que a proteção é necessária.
    5. Uma métrica das repetições para acompanhar em produção.
  • Revisar o tratamento de concorrência

    Vou colar abaixo o código de operações que leem e gravam no banco dentro de transações. Revise o tratamento de concorrência.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. Há leituras seguidas de gravações calculadas na aplicação, sem trava, condição ou versão?
    2. Há regras sobre várias linhas conferidas só com um SELECT?
    3. O nível de isolamento usado é o que o código supõe?
    4. Transações serializable são repetidas, por inteiro e só nos códigos certos?
    5. Há efeitos fora do banco dentro das transações?
    6. Há testes com pedidos simultâneos?
    
    Para cada problema, descreva a sequência de eventos que corrompe os dados e mostre a correção.
    
    Código:
    [cole aqui]