13.4. Verificações de consistência de dados no nível de aplicação #

13.4.1. Impondo consistência com transações serializáveis
13.4.2. Impondo consistência com bloqueios explícitos

É 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.

13.4.1. Impondo consistência com transações serializáveis #

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.

Atenção: Transações serializáveis ​​e replicação de dados

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.

13.4.2. Impondo consistência com bloqueios explícitos #

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.