Níveis de isolamento
O que duas transações simultâneas enxergam uma da outra e as anomalias que cada nível permite.
O problema: transações simultâneas se enxergam
O tópico de transações mostrou que uma transação é tudo ou nada. Mas, enquanto ela roda, outras rodam ao mesmo tempo, lendo e escrevendo as mesmas tabelas. O nível de isolamento decide o quanto uma enxerga das outras.
O isolamento perfeito seria cada transação rodar como se estivesse sozinha no banco, uma depois da outra. Isso tem um custo: travas, esperas e transações recusadas. Por isso os bancos oferecem níveis intermediários, que deixam passar algumas anomalias em troca de desempenho. E vêm configurados num desses níveis intermediários.
As cinco anomalias
| Anomalia | O que acontece | Exemplo |
|---|---|---|
| Leitura suja | Ler o que outra transação gravou e ainda não confirmou | Ver um saldo zerado que a outra transação vai desfazer |
| Leitura não repetível | Ler a mesma linha duas vezes e obter valores diferentes | O saldo era 100 no começo do relatório e 50 no fim |
| Fantasma | Repetir a mesma consulta e aparecerem linhas novas | A contagem de contas muda no meio do relatório |
| Atualização perdida | Duas transações leem, calculam e gravam; a segunda apaga o efeito da primeira | Dois saques de 10 a partir de 100 deixam 90 |
| Escrita enviesada | Cada transação confere uma regra e altera linhas diferentes; juntas, violam a regra | Dois médicos saem do plantão ao mesmo tempo, e ninguém fica |
As três primeiras são problemas de leitura. As duas últimas são as que corrompem dados, e são as que mais aparecem em código de aplicação.
O que cada nível permite, medido
O padrão SQL define quatro níveis: read uncommitted, read committed, repeatable read e serializable. Reproduzimos cada anomalia em cada nível, no PostgreSQL 18 e no MySQL 8.4 (InnoDB), com duas transações intercaladas passo a passo.
PostgreSQL 18 (padrão: read committed)
| Anomalia | Read committed | Repeatable read | Serializable |
|---|---|---|---|
| Leitura suja | não ocorreu | não ocorreu | não ocorreu |
| Leitura não repetível | ocorreu | não ocorreu | não ocorreu |
| Fantasma | ocorreu | não ocorreu | não ocorreu |
| Atualização perdida | ocorreu (saldo final 90) | recusada (erro 40001) | recusada (erro 40001) |
| Escrita enviesada | ocorreu (0 de plantão) | ocorreu (0 de plantão) | recusada (erro 40001) |
MySQL 8.4 (padrão: repeatable read)
| Anomalia | Read uncommitted | Read committed | Repeatable read | Serializable |
|---|---|---|---|---|
| Leitura suja | ocorreu | não ocorreu | não ocorreu | bloqueou a leitura |
| Leitura não repetível | ocorreu | ocorreu | não ocorreu | não ocorreu |
| Fantasma | ocorreu | ocorreu | não ocorreu | bloqueou a inserção |
| Atualização perdida | ocorreu | ocorreu | ocorreu (saldo final 90) | recusada (erro 1213, impasse) |
| Escrita enviesada | ocorreu | ocorreu | ocorreu | recusada (erro 1213, impasse) |
O PostgreSQL não tem leitura suja em nenhum nível: pedir read uncommitted dá read committed.
A diferença que mais engana
O nível tem o mesmo nome nos dois bancos e não se comporta igual. A linha da atualização perdida mostra isso:
- No PostgreSQL em repeatable read, a segunda transação a gravar recebeu o erro 40001 (could not serialize access due to concurrent update). O saque não se perdeu: a aplicação recebeu um erro e pode repetir.
- No MySQL em repeatable read, o nível padrão dele, a segunda transação gravou por cima, sem erro nenhum. O saldo terminou em 90 depois de dois saques de 10.
O mesmo código, com o mesmo nome de nível, está protegido num banco e perde dinheiro no outro.
O MySQL tem outra particularidade no repeatable read. A leitura enxerga uma fotografia do começo da transação, mas escritas enxergam o dado atual. Testamos: a transação contou 1 conta, outra transação inseriu uma segunda conta e confirmou, e a primeira contou de novo, ainda 1. Até aqui, sem fantasma. Então a primeira executou UPDATE contas SET saldo = saldo + 1 WHERE saldo > 0, que alterou 2 linhas, inclusive a que ela não enxergava. A contagem seguinte, na mesma transação, mostrou 2. No PostgreSQL, o mesmo UPDATE alterou 1 linha, e a contagem continuou 1.
A escrita enviesada
A anomalia mais difícil de perceber merece um exemplo completo. A regra: pelo menos um médico fica de plantão. Ana e Bia estão de plantão, e as duas pedem para sair ao mesmo tempo:
transação da Ana transação da Bia
SELECT count(*) ... de_plantao → 2
SELECT count(*) ... de_plantao → 2
"sobra uma, posso sair"
UPDATE ... WHERE medico = 'ana'
"sobra uma, posso sair"
UPDATE ... WHERE medico = 'bia'
COMMIT
COMMITCada transação conferiu a regra corretamente, com os dados que via. As duas alteraram linhas diferentes, então nenhuma trava de linha as impediu. No fim, ninguém ficou de plantão. No PostgreSQL, isso aconteceu em read committed e em repeatable read. Só o serializable recusou uma das duas.
Na Implementação, dez médicos pedem para sair ao mesmo tempo. Com read committed, os dez saíram e ninguém ficou. Com serializable e a repetição das transações recusadas, saíram nove, e um ficou, nas cinco linguagens.
Serializable: correto, e às vezes recusa
O nível serializable garante que o resultado é igual ao de alguma execução em fila, uma transação depois da outra. Os dois bancos chegam lá por caminhos diferentes:
| Banco | Como garante | Efeito para a aplicação |
|---|---|---|
| PostgreSQL | Deixa as transações rodarem e acompanha as dependências entre elas; recusa uma quando o resultado não seria serializável | Pouca espera; erro 40001 no UPDATE ou no COMMIT |
| MySQL | Transforma cada leitura numa leitura com trava compartilhada | Esperas por travas; impasses (erro 1213) e tempos de espera esgotados (erro 1205) |
Nos dois, a transação recusada precisa ser repetida do começo pela aplicação. Sem a repetição, o serializable troca dados errados por erros na cara do usuário.
Por que isso importa quando a IA escreve o código
Um assistente que escreve uma regra de negócio com transação costuma assumir que "estar numa transação" basta:
- Ler, calcular na aplicação e gravar, sem trava nem condição, no nível padrão.
- Regras que olham várias linhas ("no máximo três reservas", "pelo menos um de plantão") conferidas com um
SELECTantes de gravar. - Serializable ligado sem repetir as transações recusadas.
- Repetição de qualquer erro, e não só do erro de serialização.
- Código testado num banco e levado para outro, contando com o mesmo comportamento do mesmo nível.
O próximo nível compara as formas de proteger cada anomalia, com o custo de cada uma.
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.
Níveis de isolamento em cinco minutos
Explique em no máximo cinco minutos de leitura o que são os níveis de isolamento de um banco relacional, para alguém que já usa transações, mas nunca mudou o nível. Cubra só o essencial: o que duas transações simultâneas enxergam uma da outra, as anomalias principais, o que o nível serializável garante e por que ele exige repetir transações. Use o exemplo de saques numa conta e de médicos saindo do plantão. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
As anomalias, uma a uma
Quero entender as anomalias de concorrência em bancos relacionais. Aja como um mentor e, para cada anomalia (leitura suja, leitura não repetível, fantasma, atualização perdida e escrita enviesada), uma de cada vez: 1. Mostre uma linha do tempo com duas transações intercaladas. 2. Peça que eu diga o resultado antes de você revelar. 3. Diga em quais níveis ela acontece no PostgreSQL e no MySQL. 4. Mostre como reproduzir com dois terminais psql ou mysql. Espere a minha resposta em cada etapa.
PostgreSQL e MySQL no mesmo nível
Quero entender por que o mesmo nível de isolamento se comporta diferente no PostgreSQL e no MySQL. Explique: 1. Como o PostgreSQL implementa read committed, repeatable read e serializable (fotografias e detecção de conflitos). 2. Como o InnoDB do MySQL implementa os mesmos níveis (leituras consistentes, leituras com trava e travas de intervalo). 3. Por que a atualização perdida é recusada no repeatable read do PostgreSQL e passa no do MySQL. 4. Por que um UPDATE no repeatable read do MySQL pode alterar linhas que a transação não enxerga. 5. O que isso significa para quem escreve código que roda nos dois bancos.
Escrita enviesada na prática
Quero aprender a reconhecer e resolver a escrita enviesada. Me mostre três regras de negócio de sistemas reais que sofrem com ela, como plantão mínimo, limite de reservas e saldo somado de várias contas. Para cada uma: 1. Peça que eu explique como a regra pode ser violada. 2. Compare as soluções: serializable, trava num registro comum, restrição no banco e materializar o conflito. 3. Pergunte qual eu escolheria e por quê, e comente a minha escolha.