Isolamento de transações e problemas de concorrência

1. Introdução ao isolamento em bancos de dados relacionais

1.1. O que é concorrência em transações?

Em sistemas de banco de dados modernos, múltiplos usuários e processos acessam simultaneamente os mesmos dados. Concorrência em transações refere-se à execução paralela de várias operações de leitura e escrita no banco. Sem controle adequado, essas operações simultâneas podem gerar inconsistências e corromper os dados.

1.2. Por que o isolamento é necessário (garantindo ACID)?

O isolamento é o "I" do acrônimo ACID (Atomicidade, Consistência, Isolamento, Durabilidade). Ele garante que transações concorrentes sejam executadas como se fossem sequenciais, mesmo que na prática ocorram em paralelo. Sem isolamento, uma transação poderia ver dados intermediários de outra transação que ainda não foi confirmada, levando a decisões incorretas.

1.3. O trade-off entre isolamento e desempenho

Maior isolamento significa maior segurança, mas menor concorrência e desempenho reduzido. Níveis mais restritivos como SERIALIZABLE bloqueiam mais recursos, enquanto níveis mais relaxados como READ UNCOMMITTED permitem maior paralelismo, porém com riscos de anomalias. O desafio é equilibrar esses fatores conforme a necessidade da aplicação.

2. Problemas de concorrência clássicos

2.1. Dirty Read (Leitura suja)

Ocorre quando uma transação lê dados escritos por outra transação que ainda não foi confirmada. Se essa transação for revertida, os dados lidos são inválidos.

-- Transação A: Inicia e atualiza um saldo
BEGIN;
UPDATE contas SET saldo = saldo - 100 WHERE id = 1;

-- Transação B: Lê o saldo (não confirmado)
BEGIN;
SELECT saldo FROM contas WHERE id = 1;  -- Lê 900 (incorreto se A for revertida)

-- Transação A: Reverte a operação
ROLLBACK;

-- O saldo real volta a ser 1000, mas Transação B agiu como se fosse 900

2.2. Non-repeatable Read (Leitura não repetível)

Acontece quando uma transação lê o mesmo registro duas vezes e obtém valores diferentes, pois outra transação modificou e confirmou o dado entre as leituras.

-- Transação A: Primeira leitura
BEGIN;
SELECT saldo FROM contas WHERE id = 1;  -- Retorna 1000

-- Transação B: Atualiza e confirma
BEGIN;
UPDATE contas SET saldo = 900 WHERE id = 1;
COMMIT;

-- Transação A: Segunda leitura (mesmo registro)
SELECT saldo FROM contas WHERE id = 1;  -- Retorna 900 (diferente!)
COMMIT;

2.3. Phantom Read (Leitura fantasma)

Ocorre quando uma transação executa a mesma consulta duas vezes e obtém um conjunto diferente de linhas na segunda execução, devido a inserções ou exclusões realizadas por outra transação.

-- Transação A: Primeira consulta
BEGIN;
SELECT * FROM contas WHERE saldo > 500;  -- Retorna 3 linhas

-- Transação B: Insere uma nova conta com saldo 600
BEGIN;
INSERT INTO contas VALUES (4, 'Maria', 600);
COMMIT;

-- Transação A: Segunda consulta (mesmo filtro)
SELECT * FROM contas WHERE saldo > 500;  -- Retorna 4 linhas (linha fantasma!)
COMMIT;

2.4. Lost Update (Atualização perdida)

Duas transações leem o mesmo valor, ambas o modificam com base na leitura original, e a segunda atualização sobrescreve a primeira, resultando em perda de dados.

-- Ambas as transações leem saldo = 1000
-- Transação A: Soma 100
BEGIN;
SELECT saldo FROM contas WHERE id = 1;  -- 1000
UPDATE contas SET saldo = 1100 WHERE id = 1;

-- Transação B: Soma 200 (baseado na leitura original)
BEGIN;
SELECT saldo FROM contas WHERE id = 1;  -- 1000 (não vê atualização de A)
UPDATE contas SET saldo = 1200 WHERE id = 1;

COMMIT; -- Saldo final = 1200 (perdeu o acréscimo de 100 de A)

3. Níveis de isolamento definidos pelo padrão SQL

3.1. READ UNCOMMITTED: comportamento e riscos

O nível mais baixo de isolamento. Permite dirty reads, non-repeatable reads e phantom reads. Transações podem ler dados não confirmados de outras transações. Raramente usado em produção devido aos graves riscos de inconsistência.

3.2. READ COMMITTED: solução para dirty read

Garante que uma transação só leia dados que foram confirmados por outras transações. Elimina dirty reads, mas ainda permite non-repeatable reads e phantom reads. É o padrão no PostgreSQL e Oracle.

3.3. REPEATABLE READ: solução para non-repeatable read

Garante que, dentro de uma transação, leituras repetidas do mesmo registro retornem o mesmo valor. Elimina dirty reads e non-repeatable reads, mas ainda pode permitir phantom reads em alguns bancos. É o padrão no MySQL/InnoDB.

3.4. SERIALIZABLE: isolamento total (sem concorrência)

O nível mais alto de isolamento. Garante que transações concorrentes sejam executadas como se fossem sequenciais. Elimina todos os problemas de concorrência clássicos, mas com o maior custo de desempenho.

4. Mapeamento entre níveis de isolamento e problemas evitados

4.1. Tabela comparativa: qual nível evita qual problema?

Nível de Isolamento Dirty Read Non-repeatable Read Phantom Read Lost Update
READ UNCOMMITTED Não evita Não evita Não evita Não evita
READ COMMITTED Evita Não evita Não evita Parcialmente
REPEATABLE READ Evita Evita Depende Evita
SERIALIZABLE Evita Evita Evita Evita

4.2. Exemplos práticos de cada combinação

No MySQL/InnoDB, REPEATABLE READ também evita phantom reads através de gap locks. Já no PostgreSQL, REPEATABLE READ usa snapshot isolation, que também elimina phantom reads.

4.3. Limitações: nem todo banco implementa todos os níveis

Alguns bancos, como SQL Server, implementam todos os níveis. Outros, como Oracle e PostgreSQL, não suportam READ UNCOMMITTED (eles o tratam como READ COMMITTED). É importante conhecer as particularidades do SGBD utilizado.

5. Implementação prática dos níveis no SQL

5.1. Sintaxe SET TRANSACTION ISOLATION LEVEL

-- Sintaxe padrão SQL
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

5.2. Exemplo com READ COMMITTED (padrão PostgreSQL/Oracle)

-- PostgreSQL (READ COMMITTED é o padrão)
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

SELECT saldo FROM contas WHERE id = 1;  -- Só vê dados confirmados

-- Outra transação pode modificar e confirmar entre nossas leituras
SELECT saldo FROM contas WHERE id = 1;  -- Pode retornar valor diferente

COMMIT;

5.3. Exemplo com REPEATABLE READ (padrão MySQL/InnoDB)

-- MySQL (REPEATABLE READ é o padrão)
START TRANSACTION;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SELECT saldo FROM contas WHERE id = 1;  -- Primeira leitura: 1000

-- Mesmo que outra transação confirme uma alteração...
SELECT saldo FROM contas WHERE id = 1;  -- Segunda leitura: ainda 1000

COMMIT;  -- Após o commit, a transação vê os dados atualizados

5.4. Exemplo com SERIALIZABLE e seus custos

-- PostgreSQL
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

UPDATE contas SET saldo = saldo - 100 WHERE id = 1;
-- Se outra transação tentar modificar a mesma linha simultaneamente,
-- uma delas receberá erro de serialização e precisará ser repetida

COMMIT;

6. Mecanismos internos de isolamento

6.1. Lock-based isolation (bloqueios pessimistas)

Utiliza bloqueios explícitos em linhas, páginas ou tabelas para evitar que outras transações acessem dados simultaneamente. Pode causar deadlocks se não for bem gerenciado.

6.2. MVCC (Multiversion Concurrency Control) – isolamento otimista

Cria múltiplas versões dos registros, permitindo que leitores vejam uma versão consistente dos dados sem bloquear escritores. Usado por PostgreSQL, MySQL/InnoDB e Oracle. Oferece melhor desempenho em cenários de leitura intensa.

6.3. Snapshot isolation e como ele difere do REPEATABLE READ

Snapshot isolation fornece uma visão consistente dos dados no momento do início da transação. No PostgreSQL, REPEATABLE READ é implementado como snapshot isolation, enquanto no SQL Server, snapshot isolation é um nível separado que evita conflitos de escrita.

7. Anomalias além do padrão SQL

7.1. Write Skew (desvio de escrita)

Ocorre quando duas transações leem conjuntos de dados sobrepostos, cada uma atualiza um subconjunto diferente, mas ambas assumem condições que se invalidam mutuamente.

-- Médicos de plantão: pelo menos um deve estar presente
-- Transação A: Verifica se Dr. João pode sair
SELECT COUNT(*) FROM plantao WHERE presente = true;  -- 2 médicos
UPDATE plantao SET presente = false WHERE medico = 'João';

-- Transação B: Verifica se Dra. Maria pode sair
SELECT COUNT(*) FROM plantao WHERE presente = true;  -- 2 médicos (ainda vê João)
UPDATE plantao SET presente = false WHERE medico = 'Maria';

-- Resultado: nenhum médico presente, violando a regra de negócio

7.2. Read Skew e inconsistências de integridade referencial

Uma transação lê dados que estão em um estado inconsistente devido a atualizações parciais de outra transação.

7.3. Como SERIALIZABLE resolve essas anomalias

SERIALIZABLE detecta conflitos de serialização e aborta transações que causariam anomalias como write skew. A aplicação deve repetir a transação abortada.

8. Boas práticas e recomendações

8.1. Escolhendo o nível ideal para sua aplicação

  • READ COMMITTED: adequado para a maioria dos sistemas OLTP
  • REPEATABLE READ: para relatórios que exigem consistência durante a execução
  • SERIALIZABLE: apenas quando a integridade dos dados é crítica e a concorrência é baixa

8.2. Monitoramento de contenção e deadlocks

Configure alertas para deadlocks e monitore métricas como tempo médio de espera por bloqueios. Ferramentas como pg_stat_activity (PostgreSQL) e SHOW ENGINE INNODB STATUS (MySQL) ajudam a diagnosticar problemas.

8.3. Testes de concorrência com ferramentas

Use ferramentas como pgbench (PostgreSQL), sysbench (MySQL) ou scripts personalizados para simular carga concorrente e validar o comportamento do isolamento antes de ir para produção.

-- Exemplo com pgbench (PostgreSQL)
pgbench -c 10 -j 2 -t 1000 -f transacoes.sql minha_base

Referências