Concorrência e isolamento

Escalonamentos, serialização, leituras sujas, fantasmas, atualizações perdidas e snapshots.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Duas reservas leem stock 10 e cada aplicação escreve 9. Foram vendidas duas unidades, mas só uma foi retirada.

As operações de cada reserva podem estar corretas isoladamente e falhar quando intercaladas. O isolamento define o que uma transação pode observar e como pode interferir com as outras.

Escalonamentos seriais e serializáveis

Um escalonamento é a ordem intercalada das operações de várias transações, respeitando a ordem interna de cada uma.

É serial se uma transação acaba antes de começar a seguinte. É serializável se o seu efeito e as observações relevantes equivalem a alguma execução serial, mesmo com operações intercaladas.

Serializabilidade não escolhe necessariamente qual transação vem primeiro. Se duas transações começam ao mesmo tempo, o resultado pode equivaler a T1/T2 ou T2/T1. Ambas podem ser válidas, embora produzam respostas diferentes.

Atualização perdida

O stock inicial é 10. Duas aplicações leem o stock e depois escrevem um valor calculado fora do SGBD:

PassoT1T2
1Lê 10
2Lê 10
3Escreve 9
4Escreve 9

Foram vendidas duas unidades, mas só foi retirada uma, porque a segunda escrita sobrepõe a primeira: é uma atualização perdida.

Uma atualização relativa SET stock = stock - 1 evita aquele padrão de ler-calcular-escrever no cliente.

Uma reserva pode usar UPDATE Produto SET stock = stock - 1 WHERE id = ? AND stock >= 1 e conferir se uma linha foi afetada. Se a operação envolver várias tabelas, agrupa-as numa transação e trata os conflitos segundo o motor. Esta técnica não resolve automaticamente todas as regras entre linhas diferentes.

Leituras sujas, não repetíveis e fantasmas

Uma leitura suja observa uma alteração de outra transação antes de esta confirmar. Se T1 passa o saldo de 100 para 50, T2 lê 50 e T1 faz rollback, T2 usou um valor que nunca ficou confirmado.

Uma leitura não repetível ocorre quando uma transação lê a mesma linha duas vezes e vê valores diferentes porque outra transação confirmou uma atualização entretanto. Ler apenas dados confirmados evita sujidade, mas não assegura estabilidade entre duas instruções.

Um fantasma é uma mudança no conjunto de linhas que satisfaz um predicado. T1 conta encomendas acima de 100 euros; T2 insere e confirma uma nova encomenda desse tipo; T1 repete a contagem e obtém mais uma. A diferença não é apenas o valor de uma linha previamente lida.

Uma consulta única pode ter um snapshot coerente e duas consultas sucessivas ver snapshots diferentes. A pergunta é sempre “durante qual intervalo e sob que nível de isolamento?”.

Níveis do modelo SQL

O quadro seguinte descreve os mínimos tradicionais do modelo SQL para estas anomalias. Um motor pode oferecer garantias mais fortes.

NívelLeitura sujaNão repetívelFantasma
Read uncommittedPode ocorrerPode ocorrerPode ocorrer
Read committedProibidaPode ocorrerPode ocorrer
Repeatable readProibidaProibidaPode ocorrer
SerializableProibidaProibidaProibido

Evitar estas três anomalias não chega, por si só, para provar toda a serializabilidade. É preciso considerar dependências entre leituras e escritas e regras que envolvem várias linhas. O comportamento concreto do motor importa mais do que uma interpretação informal do nome do nível.

Grafo de precedências

Duas operações de transações diferentes entram em conflito se acedem ao mesmo item e pelo menos uma escreve. Uma leitura e outra leitura não entram em conflito.

No grafo, cada nó é uma transação. Desenha Ti→TjT_i\to T_j se uma operação de Ti precede uma operação conflitante de Tj. Um escalonamento é serializável por conflitos se e só se este grafo for acíclico. Uma ordenação topológica dá uma ordem serial equivalente por conflitos.

Considera r1(X), r2(X), w1(X), w2(X). A leitura de T1 antes da escrita de T2 dá T1→T2. A leitura de T2 antes da escrita de T1 dá T2→T1. Há um ciclo, logo não é serializável por conflitos.

Uma execução serial teria de colocar T1 antes de T2 e T2 antes de T1 ao mesmo tempo. Não existe essa ordem. A escrita w1 antes de w2 reforça T1→T2, sem criar um nó novo.

Com valores, T1 escreve em B o valor lido de A mais 10, e T2 escreve em A o valor lido de B mais 10, ambas a partir de A=B=100:

PassoT1T2
1Lê A=100
2Lê B=100
3Escreve B=110
4Escreve A=110

r1(A) antes de w2(A) dá T1→T2; r2(B) antes de w1(B) dá T2→T1. O ciclo reprova a serialização por conflitos: nenhuma ordem serial equivale a este entrelaçamento.

Agora r1(X), w1(X), r2(X), w2(X) só exige T1→T2. Equivale à ordem T1/T2. Este teste refere-se a serializabilidade por conflitos, uma condição mais restrita do que outras formas de equivalência por observações.

Bloqueios e snapshots

Um protocolo de bloqueios pode impedir que outra transação escreva um item enquanto uma leitura ou escrita precisa de o proteger. Em bloqueio em duas fases, a transação primeiro adquire bloqueios e depois liberta-os, sem adquirir novos após começar a libertar. Com as hipóteses do protocolo, isso garante serializabilidade por conflitos.

Se T1 segura A e espera B, e T2 segura B e espera A, há um deadlock. O sistema precisa de detetar ou evitar o ciclo e pode abortar uma transação. Uma aplicação deve conseguir repetir a transação inteira, não apenas a última instrução sem refazer as leituras.

TransaçãoSeguraEspera
T1AB
T2BA

Cada uma espera o que a outra segura; nenhuma avança.

Outra técnica é MVCC, controlo de concorrência multiversão. Conserva versões que permitem a leitores observar snapshots coerentes sem bloquear todas as escritas. Um snapshot coerente não garante que decisões sobre várias linhas sejam serializáveis.

Write skew: duas linhas, uma regra

Ana e Rui estão de serviço. A regra exige pelo menos uma pessoa de serviço. Cada transação lê que há duas pessoas e retira a sua própria pessoa do serviço. Se usarem o mesmo snapshot inicial e puderem confirmar escritas em linhas diferentes, o resultado pode ter zero pessoas.

Na animação, T1 e T2 mantêm o snapshot com Ana = sim e Rui = sim, mesmo quando o estado confirmado muda. O modelo de isolamento por snapshot permite os dois commits porque as escritas afetam linhas diferentes e não verifica esta regra entre linhas. Não estamos a simular SQLite.

Duas saídas decididas sobre o mesmo snapshot. T1 e T2 leem Ana e Rui de serviço. T1 retira Ana e confirma; T2 retira Rui com base no snapshot inicial e também confirma. Ficam zero pessoas. Na execução serial, T1 sai primeiro e T2 vê apenas Rui de serviço, por isso recusa a segunda saída e conserva uma pessoa.

A regra pode ser seguida sem reproduzir a animação:

ExecuçãoDepois da primeira decisãoSegunda decisãoEstado final
Snapshot partilhadoT1 retira Ana; Rui ficaT2 ainda vê duas pessoas e retira Ruininguém de serviço
Serial T1, T2T1 retira Ana; Rui ficaT2 vê só Rui e recusa a saídaRui de serviço
Serial T2, T1T2 retira Rui; Ana ficaT1 vê só Ana e recusa a saídaAna de serviço

Não houve leitura suja. Nenhuma transação sobrepôs a escrita da outra. Ainda assim, qualquer ordem serial correta faria a segunda ver só uma pessoa e recusar a saída. Esta anomalia, write skew, mostra por que isolamento por snapshot não equivale a serializabilidade.

Uma solução tem de proteger a regra completa, por exemplo através de isolamento serializável com repetição após conflito ou de um mecanismo de bloqueio que serialize decisões sobre o conjunto relevante. Bloquear apenas a linha que cada pessoa vai alterar pode não chegar.

PostgreSQL

Em PostgreSQL, READ COMMITTED é o nível por omissão e cada instrução normalmente obtém um snapshot dos dados confirmados no início dessa instrução. Duas consultas na mesma transação podem observar dados diferentes. READ UNCOMMITTED comporta-se como READ COMMITTED.

REPEATABLE READ usa um snapshot estável da transação e também impede os fantasmas descritos no quadro, oferecendo mais que o mínimo tradicional. Pode ainda permitir anomalias de serialização como write skew. SERIALIZABLE deteta dependências que podem tornar a execução incompatível com uma ordem serial e pode abortar uma transação com erro de serialização.

Este excerto é PostgreSQL, não SQLite:

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Leituras e alterações que implementam a regra.
COMMIT;

A aplicação precisa de tratar a possibilidade de erro e repetir toda a transação. SELECT ... FOR UPDATE bloqueia linhas selecionadas, mas não é uma prova genérica para toda a regra de negócio nem uma operação disponível da mesma forma em SQLite.

SQLite

SQLite permite vários leitores, mas apenas um escritor de cada vez. Em modo rollback journal, leitores e escritores podem bloquear-se. Em WAL, leitores podem continuar a ler o seu snapshot enquanto outro processo escreve; só uma escrita decorre de cada vez.

Se uma transação leitora tenta passar a escritora depois de outra ligação confirmar alterações que tornam o seu snapshot antigo, SQLite pode recusar essa promoção com SQLITE_BUSY_SNAPSHOT. É necessário terminar e repetir a transação a partir de leituras novas. BEGIN IMMEDIATE obtém cedo a capacidade de escrita, ou falha/espera se outro escritor a tiver, evitando aquela promoção tardia.

O isolamento habitual entre ligações impede leituras de alterações não confirmadas. A exceção envolve cache partilhada e PRAGMA read_uncommitted, uma configuração específica; não assumes que SQLite oferece um menu equivalente aos quatro níveis de PostgreSQL.

O bloco executável de uma página usa uma única ligação SQLite. Pode demonstrar rollback e savepoints, mas não prova comportamento concorrente entre duas ligações. Para prever esse comportamento, identifica o modo de journaling, os momentos de leitura/escrita e o tratamento dos erros de concorrência.

Exercícios

Comparar dois grafos de precedências

Compara os escalonamentos S1 e S2, respeitando a ordem interna de cada transação:

  • S1: r1(X), r2(X), w1(X), w2(X).
  • S2: r1(X), r2(Y), w1(X), r2(X), w2(X).

Desenha os grafos de precedências e decide a serializabilidade por conflitos. Há alguma ordem serial equivalente? Algum dos dois escalonamentos já é serial?

Pista
Duas leituras não conflituam. Operações sobre itens diferentes também não.
Solução

Em S1 há T1→T2 por r1(X) antes de w2(X) e por w1(X) antes de w2(X). Há T2→T1 por r2(X) antes de w1(X). O ciclo impede uma ordem serial equivalente por conflitos.

Em S2, r2(Y) não conflita com operações de T1, que só usa X. Os conflitos de X vão todos de T1 para T2: o grafo tem apenas T1→T2. É serializável por conflitos e equivale à ordem T1, T2.

Nenhum escalonamento é serial: em ambos, T2 faz uma leitura antes de T1 terminar. S2 mostra que intercalar operações não implica perder serializabilidade.

Confere a tua resposta:

  • Listei os conflitos nas duas direções de S1.
  • Obtive apenas T1→T2 em S2.
  • Distingui execução serial de execução intercalada serializável.

Erros frequentes

Não confundas grafo de precedências com grafo de espera por bloqueios. Uma aresta de precedência não afirma que a transação está à espera.
Voltar à explicação
Snapshots estáveis e regra conjunta

Ana e Rui estão ambos de serviço. Duas transações leem esse estado e cada uma retira uma pessoa diferente. Ambas confirmam, deixando zero. Identifica a anomalia e explica por que não equivale a uma ordem serial correta. Assume a regra de que deve ficar pelo menos uma pessoa de serviço.

Pista
As escritas não atingem a mesma linha.
Solução

É write skew. Cada transação decide sobre o mesmo snapshot inicial, mas escreve uma linha diferente, por isso não precisa de sobrepor a escrita da outra. Em qualquer ordem serial que aplique a regra de pelo menos uma pessoa, a segunda transação vê uma pessoa restante e não a retira. É necessário proteger a regra entre linhas, por exemplo com serializable e repetição após conflito ou um bloqueio que serialize essas decisões.

Confere a tua resposta:

  • Identifiquei write skew.
  • Comparei com ambas as ordens seriais e indiquei proteção da regra.

Erros frequentes

Não é uma leitura suja, pois o snapshot pode conter só dados confirmados. Não é a atualização perdida típica, pois as escritas são em linhas diferentes.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.