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

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.
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
- A altera
progresso_percentualde40para100, sem commit. - B lê
100. - A executa rollback.
- 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
- A lê progresso
40. - B atualiza para
60e confirma. - 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
- A conta três matrículas com
situacao = 'ativa'. - B insere outra matrícula ativa e confirma.
- 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 solicitado | Snapshot no PostgreSQL | Leitura suja | Não repetível | Fantasma | Anomalia de serialização |
|---|---|---|---|---|---|
READ UNCOMMITTED | tratado como READ COMMITTED | não | possível | possível | possível |
READ COMMITTED | novo por instrução | não | possível | possível | possível |
REPEATABLE READ | estável desde a primeira instrução relevante | não | não | não no PostgreSQL | possível |
SERIALIZABLE | estável + dependências monitoradas | não | não | não | nã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.
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”:
SELECTadquire 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 UPDATEpede 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:
SELECTsimples adquireACCESS SHAREna tabela;INSERT,UPDATE,DELETEeMERGEadquiremROW EXCLUSIVEna 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;
NOWAITfalha imediatamente se um lock necessário não puder ser obtido;SKIP LOCKEDignora 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:
- A lê contador
10. - B lê contador
10. - A grava
11. - 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
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:
- adquirir objetos na mesma ordem em todos os fluxos;
- pedir desde o início o modo mais restritivo realmente necessário;
- manter transações curtas;
- reduzir o conjunto bloqueado com predicados e índices adequados;
- tratar deadlock como falha transitória com rollback e retry da unidade completa;
- 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ção | Causa | Resultado típico | Resposta da aplicação |
|---|---|---|---|
| lock aguardando | outro detentor ainda pode terminar | instrução fica bloqueada | observar duração; evitar transação longa |
| timeout de lock/instrução | limite configurado expirou | comando é interrompido | rollback conforme estado; investigar contenção e prazo |
| deadlock | ciclo entre esperas | uma transação é abortada | repetir unidade completa com ordem consistente |
| falha de serialização | dependências não permitem ordem serial segura | transação é abortada | repetir 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
| Necessidade | Estratégia inicial possível | Cuidado principal |
|---|---|---|
| incrementar um valor | UPDATE coluna = coluna + ... | verificar limites e linhas afetadas |
| impedir edição sobre versão antiga | coluna de versão e UPDATE condicional | tratar zero linhas como conflito |
| reservar uma linha existente | SELECT ... FOR UPDATE | manter mesma transação e ordem de locks |
| distribuir itens de fila | FOR UPDATE SKIP LOCKED | resultado é propositalmente incompleto |
| proteger regra entre várias linhas | SERIALIZABLE ou lock em recurso comum | implementar retry e testar contenção |
| coordenar conceito sem linha natural | advisory lock transacional, se adequado | todos 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 READainda 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
- Qual invariável precisa sobreviver a execuções simultâneas?
- Quais linhas ou predicados cada transação lê antes de escrever?
- O nível de isolamento real é conhecido no cliente e no servidor?
- A lógica depende de repetir uma leitura com o mesmo resultado?
- Existe regra entre linhas suscetível a write skew?
- Uma atualização relativa pode substituir read-modify-write?
- O controle otimista possui versão e tratamento explícito de conflito?
- Um lock pessimista protege exatamente o recurso da decisão?
- O lock é adquirido e usado na mesma transação?
- Todos os fluxos adquirem múltiplos recursos na mesma ordem?
NOWAITouSKIP LOCKEDpreserva a semântica do caso?- Deadlock e SQLSTATE de serialização repetem a unidade completa?
- O retry possui limite, backoff e idempotência para efeitos externos?
- Transações evitam rede, interação humana e processamento dispensável?
- Há monitoramento de sessões, locks não concedidos e tempo aberto?
- Í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
- ISO — ISO/IEC 9075-2:2023, SQL/Foundation. Acesso em 30 ago. 2026.
- PostgreSQL Global Development Group — Concurrency Control: Introduction. Acesso em 30 ago. 2026.
- PostgreSQL Global Development Group — Transaction Isolation. Acesso em 30 ago. 2026.
- PostgreSQL Global Development Group — Explicit Locking. Acesso em 30 ago. 2026.
- PostgreSQL Global Development Group — SELECT locking clause. Acesso em 30 ago. 2026.
- PostgreSQL Global Development Group — Viewing Locks. Acesso em 30 ago. 2026.
- Ports, Dan R. K.; Grittner, Kevin — Serializable Snapshot Isolation in PostgreSQL. Proceedings of the VLDB Endowment, 2012.
