14 - Isolamento de Transação do Spring
Introdução
Quando vários usuários ou processos acessam os mesmos dados concomitantemente, podem ocorrer problemas inesperados. Uma transação pode ler dados que outra transação está modificando, resultando em resultados inconsistentes ou incorretos.
Para resolver esses problemas de concorrência, bancos de dados relacionais fornecem Níveis de Isolamento de Transação.
No Spring, o isolamento de transação pode ser configurado através da anotação @Transactional, permitindo que os desenvolvedores controlem como as transações interagem umas com as outras.
Compreender os níveis de isolamento é essencial para construir aplicações confiáveis, escaláveis e consistentes.
O que é Isolamento de Transação?
O Isolamento de Transação define como e quando as alterações feitas por uma transação se tornam visíveis para outras transações.
Ele controla o grau de separação entre transações sendo executadas concomitantemente.
O objetivo é equilibrar:
- Consistência dos dados
- Performance
- Concorrência
Níveis de isolamento mais altos fornecem maior consistência, mas podem reduzir a performance e a escalabilidade.
Níveis de isolamento mais baixos melhoram a concorrência, mas podem permitir leituras inconsistentes.
Por Que o Isolamento de Transação Importa
Imagine uma aplicação bancária.
Transação A:
UPDATE account
SET balance = balance - 100
WHERE id = 1;
Transação B:
SELECT balance
FROM account
WHERE id = 1;
Se a Transação B lê o saldo antes da Transação A confirmar (commit), ela pode ver dados que ainda não foram finalizados.
Isso pode levar a decisões de negócio incorretas e comportamento inconsistente da aplicação.
Os níveis de isolamento ajudam a prevenir essas situações.
Problemas Comuns de Concorrência
Antes de entender os níveis de isolamento, é importante entender os problemas que eles resolvem.
Leitura Suja (Dirty Read)
Uma transação lê dados modificados por outra transação que ainda não foi confirmada.
Exemplo
Transação A:
UPDATE account
SET balance = 500
WHERE id = 1;
Transação B:
SELECT balance
FROM account
WHERE id = 1;
Transação A posteriormente reverte (rollback):
ROLLBACK;
A Transação B leu dados que nunca existiram de fato.
Problema
Saldo Original = 1000
Saldo Temporário = 500
Reversão
Saldo Real = 1000
A Transação B usou dados inválidos.
Leitura Não Repetível (Non-Repeatable Read)
Uma transação lê a mesma linha duas vezes e obtém resultados diferentes.
Exemplo
Transação A:
SELECT balance
FROM account
WHERE id = 1;
Resultado:
1000
Transação B:
UPDATE account
SET balance = 800
WHERE id = 1;
COMMIT;
Transação A executa novamente:
SELECT balance
FROM account
WHERE id = 1;
Resultado:
800
A mesma consulta retornou valores diferentes dentro da mesma transação.
Leitura Fantasma (Phantom Read)
Uma transação executa a mesma consulta duas vezes e recebe diferentes conjuntos de linhas.
Exemplo
Transação A:
SELECT *
FROM orders
WHERE status = 'PENDING';
Retorna:
10 linhas
Transação B:
INSERT INTO orders(status)
VALUES('PENDING');
COMMIT;
Transação A executa a consulta novamente:
SELECT *
FROM orders
WHERE status = 'PENDING';
Retorna:
11 linhas
Uma nova linha apareceu como um fantasma.
Atualização Perdida (Lost Update)
Duas transações atualizam o mesmo registro simultaneamente.
Exemplo
Valor inicial:
Estoque = 10
Transação A:
Lê 10
Transação B:
Lê 10
Transação A:
Atualiza para 9
Transação B:
Atualiza para 8
Esperado:
7
Real:
8
Uma atualização foi perdida.
Níveis de Isolamento no Spring
O Spring suporta os níveis de isolamento SQL padrão através de:
@Transactional(isolation = Isolation.READ_COMMITTED)
Níveis disponíveis:
- DEFAULT
- READ_UNCOMMITTED
- READ_COMMITTED
- REPEATABLE_READ
- SERIALIZABLE
Isolation.DEFAULT
O Que Faz
Usa o nível de isolamento padrão do banco de dados.
@Transactional(isolation = Isolation.DEFAULT)
public void processOrder() {
}
Exemplos de Padrões
| Banco de Dados | Isolamento Padrão |
|---|---|
| PostgreSQL | READ_COMMITTED |
| Oracle | READ_COMMITTED |
| SQL Server | READ_COMMITTED |
| MySQL InnoDB | REPEATABLE_READ |
Vantagens
- Usa as melhores práticas do banco de dados
- Configuração simples
Desvantagens
- O comportamento pode variar entre bancos de dados
Isolation.READ_UNCOMMITTED
O Que Faz
Permite que as transações leiam dados não confirmados de outras transações.
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void generateReport() {
}
Previne
- Nada
Permite
- Leituras Sujas
- Leituras Não Repetíveis
- Leituras Fantasma
Exemplo
Transação A:
UPDATE account
SET balance = 500;
Ainda não confirmada.
Transação B:
SELECT balance FROM account;
Resultado:
500
Se a Transação A reverte, a Transação B usou dados inválidos.
Vantagens
- Performance máxima
- Maior concorrência
Desvantagens
- Menor consistência
- Raramente recomendado
Isolation.READ_COMMITTED
O Que Faz
Permite apenas a leitura de dados confirmados.
@Transactional(isolation = Isolation.READ_COMMITTED)
public void processPayment() {
}
Previne
- Leituras Sujas
Permite
- Leituras Não Repetíveis
- Leituras Fantasma
Exemplo
A Transação A atualiza dados, mas não confirma.
A Transação B tenta ler.
Resultado:
A Transação B não pode ver alterações não confirmadas.
Apenas valores confirmados são visíveis.
Vantagens
- Bom equilíbrio entre consistência e performance
- Padrão mais comum de bancos de dados
Desvantagens
- A mesma linha pode retornar valores diferentes dentro de uma transação
Isolation.REPEATABLE_READ
O Que Faz
Garante que leituras repetidas da mesma linha retornem o mesmo resultado.
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void generateStatement() {
}
Previne
- Leituras Sujas
- Leituras Não Repetíveis
Permite
- Leituras Fantasma (dependendo da implementação do banco de dados)
Exemplo
Transação A:
SELECT balance
FROM account
WHERE id = 1;
Transação B:
UPDATE account
SET balance = 500
WHERE id = 1;
Transação A lê novamente:
SELECT balance
FROM account
WHERE id = 1;
Ainda vê:
Valor Original
Vantagens
- Consistência mais forte
- Bom para operações financeiras
Desvantagens
- Mais travamento (locking)
- Concorrência reduzida
Isolation.SERIALIZABLE
O Que Faz
Fornece o mais alto nível de isolamento.
As transações se comportam como se fossem executadas uma por vez.
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transferMoney() {
}
Previne
- Leituras Sujas
- Leituras Não Repetíveis
- Leituras Fantasma
Exemplo
Transação A:
SELECT *
FROM orders
WHERE status = 'PENDING';
Enquanto a Transação A está em execução:
Transação B:
INSERT INTO orders(status)
VALUES('PENDING');
A Transação B deve esperar.
Vantagens
- Consistência máxima
- Previne todos os problemas padrão de concorrência
Desvantagens
- Menor concorrência
- Mais travamento (locking)
- Vazão (throughput) reduzida
Comparação de Níveis de Isolamento
| Nível de Isolamento | Leitura Suja | Leitura Não Repetível | Leitura Fantasma |
|---|---|---|---|
| READ_UNCOMMITTED | Possível | Possível | Possível |
| READ_COMMITTED | Prevenida | Possível | Possível |
| REPEATABLE_READ | Prevenida | Prevenida | Possível* |
| SERIALIZABLE | Prevenida | Prevenida | Prevenida |
- Dependente do banco de dados.
Configurando o Isolamento no Spring
Nível de Método
@Transactional(
isolation = Isolation.REPEATABLE_READ
)
public void processPayment() {
}
Combinado com Propagação
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED
)
public void createOrder() {
}
Combinado com Timeout
@Transactional(
isolation = Isolation.SERIALIZABLE,
timeout = 30
)
public void transferFunds() {
}
Exemplos do Mundo Real
Transferências Bancárias
Recomendado:
@Transactional(
isolation = Isolation.SERIALIZABLE
)
public void transferMoney() {
}
Motivo:
- Consistência máxima
- Sem atualizações perdidas
Processamento de Pedidos
Recomendado:
@Transactional(
isolation = Isolation.READ_COMMITTED
)
public void placeOrder() {
}
Motivo:
- Boa performance
- Previne leituras sujas
Sistemas de Relatórios
Recomendado:
@Transactional(
readOnly = true,
isolation = Isolation.READ_COMMITTED
)
public Report generateReport() {
}
Motivo:
- Execução mais rápida
- Consistente o suficiente para relatórios
Gerenciamento de Estoque
Recomendado:
@Transactional(
isolation = Isolation.REPEATABLE_READ
)
public void reserveInventory() {
}
Motivo:
- Previne cálculos de estoque inconsistentes
Erros Comuns
Usar Sempre SERIALIZABLE
Má prática:
@Transactional(
isolation = Isolation.SERIALIZABLE
)
public void findUser() {
}
Problemas:
- Travamento excessivo (excessive locking)
- Pobre escalabilidade
- Performance reduzida
Use apenas quando absolutamente necessário.
Ignorar os Padrões do Banco de Dados
Desenvolvedores frequentemente assumem que todos os bancos de dados se comportam da mesma forma.
Exemplo:
PostgreSQL -> READ_COMMITTED
MySQL -> REPEATABLE_READ
O comportamento da aplicação pode mudar após a migração.
Sempre verifique os padrões do banco de dados.
Usar Alto Isolamento para Consultas Somente Leitura
Ruim:
@Transactional(
isolation = Isolation.SERIALIZABLE,
readOnly = true
)
public List<User> findUsers() {
}
Isso oferece pouco benefício enquanto aumenta o uso de recursos.
Melhores Práticas
Comece com READ_COMMITTED
Para a maioria das aplicações:
@Transactional(
isolation = Isolation.READ_COMMITTED
)
public void processOrder() {
}
Este é geralmente o melhor equilíbrio.
Use REPEATABLE_READ para Cálculos Financeiros
Quando a consistência importa mais do que a vazão (throughput):
@Transactional(
isolation = Isolation.REPEATABLE_READ
)
public void calculateInterest() {
}
Reserve SERIALIZABLE para Operações de Negócio Críticas
Exemplos:
- Transferências bancárias
- Transações financeiras de alto valor
- Reservas críticas de estoque
Mantenha as Transações Curtas
Bom:
@Transactional
public void saveOrder() {
repository.save(order);
}
Evite:
@Transactional
public void saveOrder() {
repository.save(order);
externalApi.call();
Thread.sleep(30000);
}
Transações de longa duração aumentam o travamento (locking) e a contenção.
Entenda Seu Banco de Dados
Diferentes bancos de dados implementam o isolamento de maneiras distintas.
Sempre verifique:
- Nível de isolamento padrão
- Comportamento de travamento (locking)
- Implementação MVCC
- Implicações de performance
Resumo
O Isolamento de Transação controla como as transações concorrentes interagem entre si e determina quais dados são visíveis durante a execução da transação.
O Spring suporta cinco níveis de isolamento:
DEFAULTREAD_UNCOMMITTEDREAD_COMMITTEDREPEATABLE_READSERIALIZABLE
À medida que o isolamento aumenta, a consistência dos dados melhora, mas a concorrência e a performance geralmente diminuem.
Para a maioria das aplicações corporativas:
- Use
READ_COMMITTEDcomo a escolha padrão. - Use
REPEATABLE_READquando leituras repetidas consistentes são necessárias. - Use
SERIALIZABLEapenas para operações críticas que exigem consistência máxima.
Escolher o nível de isolamento correto é uma das decisões mais importantes no design de sistemas transacionais confiáveis e escaláveis.