É muito difícil impor regras de negócios relacionadas à integridade
de dados usando transações READ COMMITTED, porque
a visão dos dados muda a cada instrução, e até mesmo uma
única instrução pode não se restringir ao seu próprio instantâneo,
caso ocorra um conflito de escrita.
Embora a transação REPEATABLE READ possua uma
visão estável dos dados ao longo de sua execução, há um problema
sutil com o uso de instantâneos MVCC
para verificações de consistência de dados, envolvendo algo conhecido
como conflitos de leitura/escrita.
Se uma transação escrever dados, e uma transação simultânea tentar
ler os mesmos dados (antes ou depois da escrita), ela não poderá
ver o trabalho da outra transação.
A transação que lê parece então ter executado primeiro,
independentemente de qual transação foi iniciada primeiro,
ou qual foi efetivada primeiro.
Se parar por aí não há problema, mas se transação que leu também
escrever dados lidos por uma transação concorrente, agora
há uma transação que parece ter sido executada antes de qualquer
uma das transações mencionadas anteriormente.
Se a transação que parece ter sido executada por último efetivar
primeiro, é muito fácil que apareça um ciclo no gráfico da ordem
de execução das transações.
Quando este ciclo aparece, as verificações de integridade não
funcionam corretamente sem alguma ajuda.
Como mencionado na Seção 13.2.3,
as transações SERIALIZABLE são apenas transações
REPEATABLE READ que adicionam monitoramento sem
bloqueio para padrões perigosos de conflitos de leitura/escrita.
Quando é detectado um padrão que pode causar um ciclo na ordem
aparente de execução, uma das transações envolvidas é desfeita
para interromper o ciclo.
Se for usado o nível de isolamento de transação
SERIALIZABLE para todas as escritas e leituras
que precisam de uma visão consistente dos dados, não será
necessário nenhum esforço adicional para garantir a consistência.
Softwares de outros ambientes, escritos para usar transações
serializáveis para garantir a consistência, devem
“simplesmente funcionar” dessa forma no
PostgreSQL.
Ao usar essa técnica, se evita criar um fardo desnecessário para
os programadores de aplicações, se o software estiver numa
estrutura que tenta, automaticamente, executar novamente as
transações desfeitas devido à falha de serialização.
Pode ser uma boa ideia definir
default_transaction_isolation como
serializable.
Também faz sentido tomar alguma ação para garantir que nenhum outro
nível de isolamento de transação seja usado, inadvertidamente
ou para subverter as verificações de integridade, por meio de
verificações do nível de isolamento da transação nos gatilhos.
Veja a Seção 13.2.3 para obter sugestões sobre desempenho.
Este nível de proteção de integridade usando transações serializáveis ainda não se estende ao modo de espera ativa (Seção 26.4) ou réplicas lógicas. Por isto, quem usa o modo de espera ativa ou a replicação lógica pode querer usar a leitura repetível e bloqueio explícito no servidor primário.
Quando as escritas não serializáveis são possíveis, para garantir
a validade atual de uma linha e protegê-la contra atualizações
concorrentes, deve-se usar
SELECT FOR UPDATE,
SELECT FOR SHARE, ou uma instrução
LOCK TABLE apropriada.
(SELECT FOR UPDATE e
SELECT FOR SHARE bloqueiam apenas as linhas
retornadas contra atualizações concorrentes, enquanto
LOCK TABLE bloqueia toda a tabela.)
Isto deve ser considerado ao portar aplicações de
outros ambientes para o PostgreSQL.
Também é digno de nota para aqueles que convertem de outros
ambientes, que o comando SELECT FOR UPDATE
não garante que uma transação concorrente não atualize ou exclua
a linha selecionada.
Para fazer isso no PostgreSQL deve-se
atualizar realmente a linha, mesmo que nenhum valor precise ser alterado.
O comando SELECT FOR UPDATE
impede temporariamente que outras transações
adquiram o mesmo bloqueio, ou executem um comando
UPDATE ou DELETE que afetaria
a linha bloqueada, mas uma vez que a transação que mantém este
bloqueio seja efetivada ou desfeita, a transação bloqueada poderá
continuar com sua operação conflitante, a menos que um
UPDATE real da linha tenha sido executado
enquanto o bloqueio estava sendo mantido.
As verificações de validade global requerem atenção extra sob o
MVCC não serializável.
Por exemplo, uma aplicação bancária pode querer verificar se a
soma de todos os créditos em uma tabela é igual à soma dos débitos
em outra tabela, quando ambas as tabelas estiverem sendo
atualizadas ativamente.
Comparar os resultados de dois comandos sucessivos
SELECT sum(...) não funciona de forma confiável
no modo READ COMMITTED, porque a segunda consulta
incluirá, provavelmente, resultados de transações não consideradas
pela primeira transação.
Fazer as duas somas em uma única transação
REPEATABLE READ fornecerá uma imagem precisa
apenas dos efeitos das transações efetivadas antes do
início da transação REPEATABLE READ —
mas pode-se legitimamente perguntar se a resposta ainda será
relevante no momento em que for entregue.
Se a própria transação REPEATABLE READ aplicar
algumas alterações antes de tentar fazer a verificação de
consistência, a utilidade da verificação se torna ainda mais
discutível, porque agora inclui algumas, mas não todas as alterações
ocorridas após o início da transação.
Nestes casos, uma pessoa cuidadosa pode querer bloquear todas as
tabelas necessárias para a verificação, a fim de obter uma imagem
indiscutível da realidade corrente.
Um bloqueio no modo SHARE (ou superior) garante
não haver alterações não confirmadas na tabela bloqueada, exceto as
da transação corrente.
Note também que, se for depender de bloqueio explícito para
evitar alterações concorrentes, deve-se usar o modo
READ COMMITTED, ou o modo
REPEATABLE READ, e ter cuidado para obter os
bloqueios antes de realizar as consultas.
Um bloqueio REPEATABLE READ obtido por uma
transação garante que nenhuma outra transação modificando a tabela
ainda está em execução, mas se o instantâneo visto pela transação
for anterior à obtenção do bloqueio, também pode ser anterior a
algumas alterações efetivadas na tabela.
O instantâneo de uma transação REPEATABLE READ
é realmente congelado no início de sua primeira consulta
ou comando de modificação de dados
(SELECT, INSERT,
UPDATE, DELETE ou
MERGE), portanto, é possível obter bloqueios
explicitamente antes que o instantâneo seja congelado.