Pesquisar conteúdos

Encontre uma trilha ou aula pelo assunto.

Experimente pesquisar por “governança”, “ITIL” ou “planejamento”.

Concorrência, níveis de isolamento, locks, deadlocks e MVCC

Analise operações simultâneas, anomalias de leitura e mecanismos usados pelos SGBDs para coordenar concorrência.
Duas transações percorrem versões diferentes de linhas, convergem em pontos de coordenação e têm um ciclo de espera interrompido antes do resultado ordenado

Duas transações podem estar corretas quando executadas sozinhas e produzir um resultado inválido quando intercaladas. O problema não está necessariamente em um comando: está na ordem de leituras, escritas e decisões que se sobrepõem.

Na aula de transações e propriedades ACID, isolamento apareceu como uma das garantias da unidade. Agora vamos abrir essa ideia: snapshots definem o que cada transação enxerga, locks coordenam acessos incompatíveis, MVCC mantém versões e o SGBD pode esperar, detectar um ciclo ou abortar uma execução para preservar o contrato escolhido.

Ao final, você deverá conseguir:

  • reconhecer leitura suja, leitura não repetível, fantasma e anomalia de serialização;
  • comparar os níveis de isolamento efetivamente implementados pelo PostgreSQL;
  • explicar por que uma visão estável ainda pode admitir write skew;
  • diferenciar snapshot, versão de linha e lock;
  • escolher entre atualização atômica, controle otimista e lock pessimista;
  • entender por que escritores podem bloquear outros escritores mesmo com MVCC;
  • distinguir espera comum, timeout, deadlock e falha de serialização;
  • organizar locks em ordem consistente e repetir a transação completa quando necessário;
  • observar contenção antes de tentar “resolver” com mais locks.
Três painéis comparam Read Committed usando um snapshot em cada SELECT, Repeatable Read mantendo a versão inicial e Serializable monitorando dependências e podendo abortar para repetição
No PostgreSQL, o nível muda a vida do snapshot e a validação da execução. Uma transação precisa estar preparada tanto para esperar quanto para ser abortada.

Concorrência transforma sequência em interleaving

Considere duas matrículas na mesma trilha e uma regra: pelo menos um revisor deve permanecer ativo.

  • Transação A lê que Ana e Bruno estão ativos.
  • Transação B lê a mesma situação.
  • A desativa Ana porque Bruno ainda está ativo.
  • B desativa Bruno porque Ana ainda está ativa em seu snapshot.
  • As duas confirmam.

Cada transação preservou a regra segundo a visão que leu. Juntas, deixaram zero revisores ativos. Esse resultado não corresponde a nenhuma execução serial correta: se A tivesse terminado primeiro, B deveria enxergar apenas um revisor e não prosseguir — e vice-versa.

O objetivo do controle de concorrência não é proibir interleaving. É permitir paralelismo dentro de um contrato de correção conhecido.

Anomalias descrevem resultados observáveis

Os níveis do padrão SQL são definidos por fenômenos que podem ou não ocorrer. Eles são uma linguagem mínima; produtos podem oferecer garantias mais fortes no mesmo nome.

Leitura suja observa dado não confirmado

  1. A altera progresso_percentual de 40 para 100, sem commit.
  2. B lê 100.
  3. A executa rollback.
  4. B tomou uma decisão sobre um valor que nunca existiu em estado confirmado.

O PostgreSQL não permite leitura suja em nenhum de seus níveis efetivos. Solicitar READ UNCOMMITTED produz o comportamento de READ COMMITTED.

Leitura não repetível muda a mesma linha

  1. A lê progresso 40.
  2. B atualiza para 60 e confirma.
  3. A lê a mesma linha novamente e recebe 60.

Em READ COMMITTED, cada instrução começa com um snapshot novo; esse resultado pode ocorrer. Não é corrupção: é o contrato do nível.

Fantasma muda o conjunto de um predicado

  1. A conta três matrículas com situacao = 'ativa'.
  2. B insere outra matrícula ativa e confirma.
  3. A repete a consulta e conta quatro.

A “linha fantasma” é a mudança no conjunto que satisfaz o predicado, não uma alteração de valor na mesma linha previamente lida.

Anomalia de serialização não possui uma ordem serial equivalente

O exemplo dos dois revisores é um write skew: as transações leem um conjunto compartilhado, escrevem linhas diferentes e preservam seus snapshots individuais, mas o estado conjunto viola a regra.

Locks de linha sobre o que cada transação escreve não bastam, pois elas escrevem linhas distintas. A solução pode exigir serialização, um lock sobre um objeto comum ou uma modelagem que transforme a regra em conflito detectável.

Os níveis do PostgreSQL não são apenas uma escala de “segurança”

Nível solicitadoSnapshot no PostgreSQLLeitura sujaNão repetívelFantasmaAnomalia de serialização
READ UNCOMMITTEDtratado como READ COMMITTEDnãopossívelpossívelpossível
READ COMMITTEDnovo por instruçãonãopossívelpossívelpossível
REPEATABLE READestável desde a primeira instrução relevantenãonãonão no PostgreSQLpossível
SERIALIZABLEestável + dependências monitoradasnãonãonãonão entre transações confirmadas

Essa tabela descreve o PostgreSQL atual. Outro SGBD pode implementar os mesmos nomes com mecanismos e fenômenos diferentes.

READ COMMITTED prioriza o estado confirmado mais recente por instrução

É o nível padrão do PostgreSQL. Um SELECT enxerga dados confirmados antes do início daquela instrução, além das alterações anteriores da própria transação.

BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;

SELECT progresso_percentual
FROM matricula
WHERE matricula_id = 800;

-- outra transação confirma uma mudança

SELECT progresso_percentual
FROM matricula
WHERE matricula_id = 800;

COMMIT;

Os dois SELECTs podem retornar valores diferentes. Um comando único, porém, trabalha com uma visão consistente para aquela instrução.

Quando um UPDATE encontra uma linha que outra transação já alterou, pode esperar. Depois do commit concorrente, o PostgreSQL reavalia a condição sobre a versão atualizada para decidir se ainda deve modificá-la. Isso torna resultados dependentes do predicado e da concorrência; confira linhas afetadas.

REPEATABLE READ estabiliza a visão da transação

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SELECT COUNT(*)
FROM matricula
WHERE situacao = 'ativa';

-- commits posteriores de outras transações não entram no snapshot

SELECT COUNT(*)
FROM matricula
WHERE situacao = 'ativa';

COMMIT;

Os dois resultados usam o mesmo snapshot. No PostgreSQL, esse nível é implementado como snapshot isolation e impede também fantasmas, uma garantia superior ao mínimo exigido pelo padrão para REPEATABLE READ.

Visão estável não significa execução serial. Transações podem ler o mesmo estado e escrever linhas distintas, produzindo write skew. Atualizações conflitantes também podem causar erro de serialização, exigindo retry desde o início.

SERIALIZABLE protege a equivalência serial por aborto

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- ler fatos, decidir e escrever
COMMIT;

No PostgreSQL, SERIALIZABLE combina snapshot com Serializable Snapshot Isolation (SSI). O servidor monitora dependências de leitura e escrita que poderiam formar um resultado sem ordem serial válida. Para romper a estrutura perigosa, uma transação pode falhar com SQLSTATE 40001.

Isso não significa executar uma de cada vez. Transações continuam concorrentes; somente um conjunto de commits incompatível é impedido. A aplicação precisa repetir toda a transação, refazendo leituras e decisões em um novo snapshot.

MVCC permite snapshots sobre versões

MVCC significa Multiversion Concurrency Control. Em vez de sobrescrever imediatamente uma linha como se existisse uma única cópia visível para todos, o PostgreSQL cria versões de tuplas e decide qual delas cada snapshot pode enxergar.

Um snapshot antigo vê a versão 1 confirmada enquanto uma versão 2 é criada; após o commit, um snapshot futuro vê a versão 2, e a versão anterior permanece enquanto ainda puder ser necessária
A linha é uma identidade lógica; o mecanismo mantém versões físicas com regras de visibilidade. Versão não é cópia independente do banco.

Snapshot é uma regra de visibilidade

Um snapshot responde quais transações e versões são visíveis naquele ponto lógico. Ele não é necessariamente um arquivo nem uma duplicação física do banco.

Se B atualiza uma linha enquanto A mantém um snapshot anterior:

  • A pode continuar lendo a versão antiga confirmada;
  • B cria uma nova versão para sua alteração;
  • antes do commit de B, outras transações não tratam a nova versão como confirmada;
  • snapshots futuros podem ver a nova versão depois do commit;
  • a versão antiga só pode ser removida quando não for mais necessária para visibilidade e recuperação.

Leitura não bloquear escrita tem limites importantes

No fluxo comum do PostgreSQL, uma consulta simples pode ler a versão visível sem bloquear o escritor, e o escritor não precisa impedir essa leitura. Mas isso não significa “nunca há lock”:

  • SELECT adquire lock de tabela compatível com DML, mas pode conflitar com certas alterações estruturais;
  • dois escritores da mesma linha precisam ser coordenados;
  • SELECT ... FOR UPDATE pede lock de linha explicitamente;
  • comandos DDL podem exigir locks fortes;
  • serialização pode abortar por dependências mesmo sem uma espera tradicional.

Versões antigas exigem manutenção

Atualizações e exclusões deixam versões que se tornam obsoletas. O PostgreSQL usa VACUUM para recuperar espaço reutilizável e manter informações necessárias ao controle de visibilidade.

Transações antigas podem prolongar a necessidade de versões anteriores. Por isso, uma transação aberta e ociosa não é apenas um problema de conexão: ela pode interferir no ciclo de manutenção e aumentar crescimento de tabelas.

Locks coordenam operações incompatíveis

Um lock representa uma permissão temporária sobre um recurso. O ponto central não é “bloqueado ou desbloqueado”, mas quais modos entram em conflito.

O PostgreSQL adquire locks automaticamente:

  • SELECT simples adquire ACCESS SHARE na tabela;
  • INSERT, UPDATE, DELETE e MERGE adquirem ROW EXCLUSIVE na tabela alvo;
  • atualizações e exclusões também bloqueiam linhas específicas;
  • alterações estruturais podem adquirir modos mais restritivos;
  • locks normalmente permanecem até o fim da transação.

Lock de tabela e lock de linha são camadas diferentes. O nome ROW EXCLUSIVE, apesar de histórico, identifica um modo de tabela; não significa que toda a tabela ficou indisponível para leitura.

SELECT FOR UPDATE reserva as linhas da decisão

Suponha uma fila em que um trabalhador precisa escolher e assumir uma tarefa:

BEGIN;

SELECT tarefa_id
FROM tarefa
WHERE situacao = 'pendente'
ORDER BY prioridade DESC, tarefa_id
LIMIT 1
FOR UPDATE;

UPDATE tarefa
SET situacao = 'processando'
WHERE tarefa_id = $1;

COMMIT;

FOR UPDATE impede que outra transação modifique, exclua ou adquira lock conflitante sobre a linha até o término. Uma consulta simples ainda pode ler a versão visível.

O lock precisa permanecer dentro de uma transação que usa a decisão. Fazer SELECT FOR UPDATE em autocommit e atualizar depois em outra transação libera a proteção cedo demais.

NOWAIT e SKIP LOCKED mudam a política de espera

SELECT tarefa_id
FROM tarefa
WHERE situacao = 'pendente'
ORDER BY prioridade DESC, tarefa_id
LIMIT 1
FOR UPDATE SKIP LOCKED;
  • NOWAIT falha imediatamente se um lock necessário não puder ser obtido;
  • SKIP LOCKED ignora linhas indisponíveis em vez de esperar.

SKIP LOCKED oferece uma visão deliberadamente incompleta e é útil em consumidores de fila. Não o use em relatórios ou decisões que precisam considerar todas as linhas elegíveis.

Controle otimista detecta uma versão inesperada

Quando conflitos são raros, a aplicação pode manter uma coluna de versão:

UPDATE conteudo
SET
  titulo = $1,
  versao = versao + 1
WHERE conteudo_id = $2
  AND versao = $3
RETURNING versao;

Zero linhas retornadas indica que o estado mudou ou o registro não existe. A aplicação decide recarregar, mesclar, rejeitar ou repetir. Isso evita manter um lock entre a leitura e uma interação longa com o usuário.

Atualização relativa evita read-modify-write desnecessário

Este fluxo é vulnerável:

  1. A lê contador 10.
  2. B lê contador 10.
  3. A grava 11.
  4. B grava 11.

Um incremento foi perdido. Quando a regra permite, expresse a operação diretamente:

UPDATE trilha
SET total_acessos = total_acessos + 1
WHERE trilha_id = 10;

O servidor coordena escritores da linha e aplica o incremento sobre a versão apropriada. Nem toda decisão pode ser reduzida a uma expressão atômica, mas vale procurar essa forma antes de adicionar locks manuais.

Espera, timeout e deadlock são situações diferentes

Espera simples possui uma direção

  • A detém um lock sobre a linha 42.
  • B solicita um modo conflitante e espera A terminar.
  • A confirma ou desfaz.
  • B continua.

Há contenção, mas não ciclo.

Timeout é uma política de limite

Uma aplicação ou configuração pode limitar quanto tempo uma instrução ou aquisição de lock aguarda. O timeout interrompe a espera; ele não prova que existia deadlock.

Deadlock forma um ciclo

A transação azul detém a linha A e espera B, enquanto a laranja detém B e espera A; o servidor rompe o ciclo abortando uma vítima, e a solução ordena todos os locks como A antes de B
A defesa principal é adquirir múltiplos recursos numa ordem global consistente. Ainda assim, a aplicação precisa tratar a transação escolhida como vítima.

Exemplo com duas sessões:

-- Transação A
UPDATE matricula SET progresso = 60 WHERE matricula_id = 1;
-- depois tenta matricula_id = 2

-- Transação B
UPDATE matricula SET progresso = 70 WHERE matricula_id = 2;
-- depois tenta matricula_id = 1

A espera B liberar 2; B espera A liberar 1. O PostgreSQL detecta o ciclo e aborta uma transação. Não existe garantia de qual será a vítima.

As principais defesas são:

  1. adquirir objetos na mesma ordem em todos os fluxos;
  2. pedir desde o início o modo mais restritivo realmente necessário;
  3. manter transações curtas;
  4. reduzir o conjunto bloqueado com predicados e índices adequados;
  5. tratar deadlock como falha transitória com rollback e retry da unidade completa;
  6. registrar contexto, tentativa e duração para investigar recorrência.

Falha de serialização e deadlock pedem retry, mas não são iguais

SituaçãoCausaResultado típicoResposta da aplicação
lock aguardandooutro detentor ainda pode terminarinstrução fica bloqueadaobservar duração; evitar transação longa
timeout de lock/instruçãolimite configurado expiroucomando é interrompidorollback conforme estado; investigar contenção e prazo
deadlockciclo entre esperasuma transação é abortadarepetir unidade completa com ordem consistente
falha de serializaçãodependências não permitem ordem serial seguratransação é abortadarepetir unidade completa em novo snapshot

O retry deve ter limite e backoff. Uma tempestade de retries pode agravar contenção. Operações externas precisam de idempotência, como discutido na aula anterior.

Observe antes de escolher um mecanismo

No PostgreSQL, pg_stat_activity mostra sessões, estados e eventos de espera; pg_locks mostra locks concedidos ou aguardados. Uma inspeção inicial pode relacioná-los:

SELECT
  a.pid,
  a.state,
  a.wait_event_type,
  a.wait_event,
  a.query_start,
  l.locktype,
  l.mode,
  l.granted
FROM pg_stat_activity AS a
LEFT JOIN pg_locks AS l
  ON l.pid = a.pid
WHERE a.datname = current_database()
ORDER BY a.query_start, a.pid;

Esse resultado pode conter várias linhas por sessão porque uma transação mantém vários locks. Use ferramentas administrativas com privilégios apropriados e evite expor textos de consultas ou identificadores sensíveis.

Perguntas úteis:

  • qual sessão está esperando?
  • há quanto tempo a transação está aberta?
  • qual recurso e modo não foram concedidos?
  • quem detém o modo conflitante?
  • a sessão está idle in transaction?
  • o padrão ocorre numa rota, lote ou migração específica?
  • a consulta examina linhas demais antes de encontrar o alvo?

Não finalize sessões automaticamente apenas por duração sem compreender impacto. Cancelar consulta, encerrar sessão e aguardar são ações diferentes; todas precisam de procedimento operacional.

Escolha a estratégia pela invariável

NecessidadeEstratégia inicial possívelCuidado principal
incrementar um valorUPDATE coluna = coluna + ...verificar limites e linhas afetadas
impedir edição sobre versão antigacoluna de versão e UPDATE condicionaltratar zero linhas como conflito
reservar uma linha existenteSELECT ... FOR UPDATEmanter mesma transação e ordem de locks
distribuir itens de filaFOR UPDATE SKIP LOCKEDresultado é propositalmente incompleto
proteger regra entre várias linhasSERIALIZABLE ou lock em recurso comumimplementar retry e testar contenção
coordenar conceito sem linha naturaladvisory lock transacional, se adequadotodos os participantes devem obedecer ao protocolo

Advisory locks têm significado definido pela aplicação. O banco não deduz qual dado eles protegem. Prefira a variante vinculada à transação para unidades curtas e documente como a chave é formada; colisões ou participantes que ignoram o acordo anulam a proteção.

Erros comuns

  • usar “isolamento alto” sem política de retry: conflitos corretos viram falhas para o usuário;
  • supor que READ COMMITTED repete a leitura: cada instrução pode obter snapshot novo;
  • confundir snapshot estável com serialização: REPEATABLE READ ainda admite write skew;
  • imaginar que MVCC elimina locks: escritores e operações estruturais ainda entram em conflito;
  • usar SELECT FOR UPDATE fora da transação efetiva: o lock é liberado antes da mudança;
  • bloquear mais linhas do que o requisito: contenção e deadlocks aumentam;
  • usar SKIP LOCKED em relatório: linhas relevantes desaparecem da visão;
  • fazer read-modify-write na aplicação para um incremento simples: atualizações podem ser perdidas;
  • não verificar a versão no controle otimista: a última gravação vence silenciosamente;
  • chamar toda espera de deadlock: espera unidirecional pode terminar normalmente;
  • tratar timeout como detecção de ciclo: ele apenas aplica um prazo;
  • depender de qual transação será vítima: o PostgreSQL não oferece essa escolha como contrato;
  • adquirir recursos em ordens diferentes: ciclos tornam-se prováveis;
  • repetir apenas o comando que falhou: as leituras da transação perderam validade;
  • manter sessão idle in transaction: locks e snapshots continuam retidos;
  • adicionar locks por palpite: correção pode vir com contenção desnecessária ou continuar incompleta.

Checklist de concorrência

  1. Qual invariável precisa sobreviver a execuções simultâneas?
  2. Quais linhas ou predicados cada transação lê antes de escrever?
  3. O nível de isolamento real é conhecido no cliente e no servidor?
  4. A lógica depende de repetir uma leitura com o mesmo resultado?
  5. Existe regra entre linhas suscetível a write skew?
  6. Uma atualização relativa pode substituir read-modify-write?
  7. O controle otimista possui versão e tratamento explícito de conflito?
  8. Um lock pessimista protege exatamente o recurso da decisão?
  9. O lock é adquirido e usado na mesma transação?
  10. Todos os fluxos adquirem múltiplos recursos na mesma ordem?
  11. NOWAIT ou SKIP LOCKED preserva a semântica do caso?
  12. Deadlock e SQLSTATE de serialização repetem a unidade completa?
  13. O retry possui limite, backoff e idempotência para efeitos externos?
  14. Transações evitam rede, interação humana e processamento dispensável?
  15. Há monitoramento de sessões, locks não concedidos e tempo aberto?
  16. Índices e predicados reduzem o trabalho até encontrar as linhas alvo?

O que você deve guardar

Isolamento define quais interleavings podem produzir commits. READ COMMITTED renova o snapshot por instrução; REPEATABLE READ estabiliza a visão, mas não garante toda ordem serial; SERIALIZABLE rejeita combinações perigosas e exige retry. MVCC oferece versões para leitura concorrente, enquanto locks continuam coordenando operações incompatíveis. Deadlock é um ciclo detectável; timeout é apenas um limite de espera.

Na próxima aula, veremos como índices, estatísticas e planos de execução influenciam o caminho usado para localizar linhas — e, consequentemente, o tempo e a extensão do trabalho concorrente.

Referências