Imagina que você tá jogando um game online, e aí algum cheater mexe no código do jogo e deixa o personagem dele mais forte, ou então você tá lendo um livro na biblioteca e alguém pode trocar as páginas, colocar capítulos novos ou até trocar o livro inteiro. Bem chato, né? É justamente desses "sustos" que o nível de isolamento REPEATABLE READ te protege.
REPEATABLE READ garante que os dados que você vê dentro de uma transação vão ficar iguais até o final dela. Mesmo que outra transação tente atualizar esses dados, a sua transação tá blindada contra essas mudanças.
Principais pontos:
- Previne
Dirty Read(leitura de dados que ainda não foram confirmados). - E, o mais importante, previne
Non-Repeatable Read. Isso significa que se você leu um conjunto de dados no começo da transação, ao ler de novo, vai receber os mesmos dados, mesmo que outro usuário tenha mudado eles.
Mas o REPEATABLE READ não protege contra Phantom Read. Se outra transação adicionar novas linhas, elas podem aparecer no seu novo select. Pra resolver até esse problema, você vai precisar do nível SERIALIZABLE, mas isso a gente vê depois.
Como configurar o nível de isolamento REPEATABLE READ
Antes de ir pros exemplos, bora ver como ativar esse nível de isolamento no PostgreSQL. Tem dois jeitos principais:
Definir o nível de isolamento pra uma transação específica:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; -- Seus comandos COMMIT;Definir o nível de isolamento pra sessão atual:
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
No segundo caso, todas as transações dessa sessão vão usar REPEATABLE READ.
Exemplo: prevenindo Non-Repeatable Read
Vamos supor que temos uma tabela accounts com a seguinte estrutura:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pendente'
);
INSERT INTO orders (customer_name, status)
VALUES ('Alice', 'pendente'), ('Bob', 'pendente');
Vamos começar com um cenário básico, onde uma transação altera os dados e outra lê.
Cenário sem REPEATABLE READ (nível READ COMMITTED)
Transação 1 começa:
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- Recebe: 100
Enquanto isso, Transação 2 muda os dados:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
Transação 1 continua:
SELECT balance FROM accounts WHERE account_id = 1;
-- Recebe: 150 (os dados mudaram!)
COMMIT;
Como dá pra ver, no nível READ COMMITTED os dados podem mudar entre duas leituras dentro da mesma transação. Isso é o tal do Non-Repeatable Read.
Cenário com REPEATABLE READ
Agora vamos tentar o mesmo exemplo, mas com o nível de isolamento REPEATABLE READ.
Transação 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- Recebe: 100
Transação 2:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
Transação 1 continua:
SELECT balance FROM accounts WHERE account_id = 1;
-- Ainda recebe: 100 (os dados não mudaram!)
COMMIT;
Não importa o que a outra transação fez, a transação 1 vê os dados do jeito que estavam quando ela começou. Assim, o Non-Repeatable Read é evitado.
Como funciona o REPEATABLE READ
O PostgreSQL usa o esquema MVCC (Multi-Version Concurrency Control) pra implementar o nível de isolamento REPEATABLE READ. O lance do MVCC é que cada transação pega um "snapshot" estável do banco, que não muda até ela acabar. Isso rola porque o banco cria e gerencia várias versões das linhas.
Quando a transação começa, ela vê os dados do jeito que estavam naquele momento. Se outra transação faz mudanças, o PostgreSQL cria uma nova versão da linha, mas a versão antiga continua lá pra quem ainda tá usando.
Por isso as transações podem ficar lentas e consumir mais memória. E é por isso também que pouca gente usa o nível mais forte de isolamento: ele é o mais seguro, mas também o que mais deixa o banco lento.
Limitações do REPEATABLE READ: Phantom Read
Como já falamos, REPEATABLE READ não protege contra Phantom Read. Pra entender isso, olha um exemplo com selects que pegam faixas de dados.
Imagina que temos uma tabela orders:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
amount NUMERIC NOT NULL
);
INSERT INTO orders (amount)
VALUES (50), (100), (150);
Transação 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- Recebe: 2
Transação 2:
BEGIN;
INSERT INTO orders (amount) VALUES (200);
COMMIT;
Transação 1 continua:
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- Recebe: 3 (apareceu uma linha nova no resultado!)
COMMIT;
Nesse caso, a linha nova (com amount = 200) foi adicionada por outra transação, e ela "apareceu do nada" no resultado do select da transação 1, mesmo com o nível REPEATABLE READ.
Se você quiser evitar Phantom Read, vai ter que usar o nível SERIALIZABLE, mas aí sempre rola um trade-off com performance.
Vantagens e desvantagens do REPEATABLE READ
O nível de isolamento REPEATABLE READ é ótimo quando você precisa ter certeza que os dados não vão mudar enquanto a transação tá rolando. Assim que você lê alguma coisa, esse valor vai ficar igual até o COMMIT, mesmo que alguém em outra transação tente mudar.
Esse esquema previne tanto dirty read quanto non-repeatable read. Você trabalha sempre com os mesmos dados do começo ao fim — nada de atualização surpresa "no meio do caminho". Isso é muito útil quando você tá gerando relatórios ou tomando decisões onde a consistência é importante.
Por outro lado, REPEATABLE READ não resolve o problema dos "fantasmas" (phantom read) — quando aparecem linhas novas no resultado de um select que você já fez na mesma transação. E, se o sistema tiver muita concorrência, esse nível pode causar conflitos entre transações, principalmente se elas acessam os mesmos dados. Isso pode gerar locks e rollbacks, mesmo que os comandos estejam certos.
No geral, REPEATABLE READ é um bom equilíbrio entre segurança e performance, mas em cenários com muita concorrência pode precisar de ajustes e atenção extra.
Dicas úteis e erros comuns
- Lembra que o nível de isolamento afeta a performance. Usa
REPEATABLE READsó quando você realmente precisa garantir que os dados não vão mudar. - Confundir
REPEATABLE READcomSERIALIZABLEé erro clássico. Se aparecerem linhas novas num select repetido, isso é esperado noREPEATABLE READ. - Se for trabalhar com transações longas, fica esperto com possíveis conflitos de lock. Transações demoradas podem travar outras operações.
O PostgreSQL tem várias ferramentas pra gerenciar isolamento de transações. O nível REPEATABLE READ é perfeito quando você precisa garantir que os dados lidos numa transação não vão mudar até o final dela.
GO TO FULL VERSION