Mat e Erwin estão certos, e estou adicionando outra resposta para expandir ainda mais o que eles disseram de uma maneira que não cabe em um comentário. Como suas respostas parecem não satisfazer a todos, houve uma sugestão de que os desenvolvedores do PostgreSQL devessem ser consultados, e eu sou um deles, irei elaborar.
O ponto importante aqui é que, sob o padrão SQL, dentro de uma transação em execução no READ COMMITTED
nível de isolamento da transação, a restrição é que o trabalho de transações não confirmadas não deve estar visível. Quando o trabalho das transações confirmadas se torna visível, depende da implementação. O que você está apontando é a diferença de como dois produtos optaram por implementá-lo. Nenhuma implementação está violando os requisitos do padrão.
Aqui está o que acontece no PostgreSQL, em detalhes:
S1-1 é executado (1 linha excluída)
A linha antiga é deixada no lugar, porque o S1 ainda pode reverter, mas o S1 agora mantém um bloqueio na linha, para que qualquer outra sessão que tente modificar a linha espere para ver se o S1 confirma ou retrocede. Qualquer leitura da tabela ainda pode ver a linha antiga, a menos que tente travá-la com SELECT FOR UPDATE
ou SELECT FOR SHARE
.
S2-1 é executado (mas está bloqueado porque S1 tem um bloqueio de gravação)
S2 agora precisa esperar para ver o resultado de S1. Se o S1 reverter em vez de confirmar, o S2 excluirá a linha. Observe que se o S1 inserisse uma nova versão antes de reverter, a nova versão nunca estaria lá da perspectiva de qualquer outra transação, nem a versão antiga teria sido excluída da perspectiva de qualquer outra transação.
Execuções S1-2 (1 linha inserida)
Esta linha é independente da antiga. Se houvesse uma atualização da linha com id = 1, as versões antiga e nova seriam relacionadas e o S2 poderia excluir a versão atualizada da linha quando ela fosse desbloqueada. O fato de uma nova linha ter os mesmos valores que uma linha que existia no passado não a torna igual a uma versão atualizada dessa linha.
S1-3 é executado, liberando o bloqueio de gravação
Portanto, as alterações do S1 são persistentes. Uma linha se foi. Uma linha foi adicionada.
S2-1 é executado, agora que pode ser bloqueado. Mas relata 0 linhas excluídas. HÃ???
O que acontece internamente é que existe um ponteiro de uma versão de uma linha para a próxima versão dessa mesma linha, se ela for atualizada. Se a linha for excluída, não há próxima versão. Quando uma READ COMMITTED
transação é despertada de um bloco em um conflito de gravação, segue-se essa cadeia de atualização até o fim; se a linha não tiver sido excluída e se ainda atender aos critérios de seleção da consulta, ela será processada. Esta linha foi excluída, portanto, a consulta do S2 segue em frente.
O S2 pode ou não chegar à nova linha durante a verificação da tabela. Se isso acontecer, verá que a nova linha foi criada após o DELETE
início da instrução S2 e, portanto, não faz parte do conjunto de linhas visíveis para ela.
Se o PostgreSQL reiniciasse toda a instrução DELETE do S2 desde o início com um novo instantâneo, ele se comportaria da mesma maneira que o SQL Server. A comunidade do PostgreSQL não escolheu fazer isso por razões de desempenho. Nesse caso simples, você nunca notaria a diferença no desempenho, mas se você tivesse dez milhões de linhas em um DELETE
quando foi bloqueado, certamente o faria. Aqui há uma troca onde o PostgreSQL escolheu o desempenho, uma vez que a versão mais rápida ainda cumpre os requisitos do padrão.
S2-2 é executado, relata uma violação exclusiva de restrição de chave
Obviamente, a linha já existe. Esta é a parte menos surpreendente da imagem.
Embora exista algum comportamento surpreendente aqui, tudo está em conformidade com o padrão SQL e dentro dos limites do que é "específico da implementação" de acordo com o padrão. Certamente pode ser surpreendente se você estiver assumindo que o comportamento de alguma outra implementação estará presente em todas as implementações, mas o PostgreSQL se esforça muito para evitar falhas de serialização no READ COMMITTED
nível de isolamento e permite alguns comportamentos que diferem de outros produtos para conseguir isso.
Agora, pessoalmente, não sou um grande fã do READ COMMITTED
nível de isolamento de transações na implementação de nenhum produto. Todos eles permitem que as condições de corrida criem comportamentos surpreendentes do ponto de vista transacional. Quando alguém se acostuma aos comportamentos estranhos permitidos por um produto, eles tendem a considerar o "normal" e as compensações escolhidas por outro produto como estranhas. Mas todo produto precisa fazer algum tipo de troca por qualquer modo que não seja realmente implementado SERIALIZABLE
. Onde os desenvolvedores do PostgreSQL optaram por chamar a atenção READ COMMITTED
é minimizar o bloqueio (leituras não bloqueiam gravações e gravações não bloqueiam leituras) e minimizar as chances de falhas de serialização.
O padrão exige que as SERIALIZABLE
transações sejam o padrão, mas a maioria dos produtos não faz isso porque causa um impacto no desempenho nos níveis de isolamento de transação mais frouxos. Alguns produtos nem fornecem transações verdadeiramente serializáveis quando SERIALIZABLE
são escolhidos - principalmente o Oracle e as versões do PostgreSQL anteriores à 9.1. Porém, usar verdadeiramente SERIALIZABLE
transações é a única maneira de evitar efeitos surpreendentes das condições de corrida, e as SERIALIZABLE
transações sempre devem bloquear para evitar as condições de corrida ou reverter algumas transações para evitar uma condição de corrida em desenvolvimento. A implementação mais comum de SERIALIZABLE
transações é o bloqueio estrito de duas fases (S2PL), que apresenta falhas de bloqueio e de serialização (na forma de deadlocks).
Divulgação completa: Trabalhei com Dan Ports do MIT para adicionar transações verdadeiramente serializáveis ao PostgreSQL versão 9.1 usando uma nova técnica chamada Serializable Snapshot Isolation.
READ COMMITTED
transações, você tem uma condição de corrida: o que aconteceria se outra transação inserisse uma nova linha após oDELETE
início e antes do início da segundaDELETE
? Com transações menos rigorosas do queSERIALIZABLE
as duas principais maneiras de fechar as condições de corrida, é através da promoção de um conflito (mas isso não ajuda quando a linha está sendo excluída) e a materialização de um conflito. Você pode materializar o conflito tendo uma tabela "id" atualizada para cada linha excluída ou bloqueando explicitamente a tabela. Ou use tentativas com erro.Acredito que isso ocorra por design, de acordo com a descrição do nível de isolamento de confirmação de leitura do PostgreSQL 9.2:
A linha que você inserir no
S1
ainda não existia quandoS2
éDELETE
iniciada. Portanto, não será visto pela exclusãoS2
conforme ( 1 ) acima. O queS1
excluiu é ignorado porS2
's deDELETE
acordo com ( 2 ).Assim
S2
, a exclusão não faz nada. Quando a inserção vem junto, porém, que se faz verS1
de inserção:Portanto, a tentativa de inserção
S2
falha com a violação de restrição.Continuar lendo esse documento, usar leitura repetível ou até serializável não resolveria o problema completamente - a segunda sessão falharia com um erro de serialização na exclusão.
Isso permitiria tentar novamente a transação.
fonte
Concordo plenamente com a excelente resposta de @ Mat . Só escrevo outra resposta, porque não caberia em um comentário.
Em resposta ao seu comentário: O
DELETE
no S2 já está conectado a uma versão de linha específica. Como isso é eliminado pelo S1 nesse meio tempo, o S2 se considera bem-sucedido. Embora não seja óbvio à primeira vista, a série de eventos é praticamente assim:É tudo por design. Você realmente precisa usar
SERIALIZABLE
transações para seus requisitos e tente novamente com falha de serialização.fonte
Use uma chave primária DEFERRABLE e tente novamente.
fonte
Também enfrentamos esse problema. Nossa solução está adicionando
select ... for update
antesdelete from ... where
. O nível de isolamento deve ser Read Confirmado.fonte