Mat i Erwin mają rację, a ja dodam tylko kolejną odpowiedź, aby rozwinąć to, co powiedzieli, w sposób, który nie mieści się w komentarzu. Ponieważ ich odpowiedzi nie wydają się satysfakcjonujące dla wszystkich i pojawiła się sugestia, że należy skonsultować się z programistami PostgreSQL, a ja jestem jednym z nich, omówię to szczegółowo.
Ważną kwestią jest to, że zgodnie ze standardem SQL w ramach transakcji działającej na READ COMMITTED
poziomie izolacji transakcji ograniczenie polega na tym, że praca niezaangażowanych transakcji nie może być widoczna. Kiedy praca zatwierdzonych transakcji staje się widoczna, zależy od implementacji. Wskazujesz na różnicę w sposobie, w jaki dwa produkty zdecydowały się to wdrożyć. Żadna z implementacji nie narusza wymagań normy.
Oto, co dzieje się w PostgreSQL:
Uruchamia S1-1 (usunięto 1 wiersz)
Stary wiersz pozostaje na miejscu, ponieważ S1 nadal może się wycofać, ale S1 trzyma teraz blokadę w wierszu, dzięki czemu każda inna sesja próbująca zmodyfikować wiersz będzie czekać na sprawdzenie, czy S1 się wycofuje, czy wycofuje. Wszelkie odczyty tabeli nadal widzą stary wiersz, chyba że spróbują go zablokować za pomocą SELECT FOR UPDATE
lub SELECT FOR SHARE
.
S2-1 działa (ale jest zablokowane, ponieważ S1 ma blokadę zapisu)
S2 musi teraz poczekać, aby zobaczyć wynik S1. Jeśli S1 miałby wycofać, a nie zatwierdzić, S2 usunąłby wiersz. Zauważ, że gdyby S1 wstawił nową wersję przed wycofaniem, nowa wersja nigdy nie byłaby tam z perspektywy innej transakcji, ani też stara wersja nie zostałaby usunięta z perspektywy jakiejkolwiek innej transakcji.
S1-2 działa (wstawiono 1 wiersz)
Ten wiersz jest niezależny od starego. Gdyby była aktualizacja wiersza o id = 1, stare i nowe wersje byłyby powiązane, a S2 mógłby usunąć zaktualizowaną wersję wiersza, gdy został odblokowany. To, że nowy wiersz ma takie same wartości, jak jakiś wiersz, który istniał w przeszłości, nie czyni go tym samym, co zaktualizowana wersja tego wiersza.
S1-3 działa, zwalniając blokadę zapisu
Tak więc zmiany S1 są utrwalane. Jeden rząd zniknął. Dodano jeden wiersz.
S2-1 działa, teraz, gdy może uzyskać blokadę. Ale raporty 0 wierszy usunięte. Co ???
To, co dzieje się wewnętrznie, polega na tym, że istnieje wskaźnik z jednej wersji wiersza do następnej wersji tego samego wiersza, jeśli jest aktualizowany. Jeśli wiersz zostanie usunięty, nie będzie następnej wersji. Gdy READ COMMITTED
transakcja budzi się z bloku konfliktu zapisu, następuje ten łańcuch aktualizacji do końca; jeśli wiersz nie został usunięty i jeśli nadal spełnia kryteria wyboru zapytania, zostanie przetworzony. Ten wiersz został usunięty, więc zapytanie S2 przechodzi dalej.
S2 może, ale nie musi dostać się do nowego wiersza podczas skanowania tabeli. Jeśli tak, zobaczy, że nowy wiersz został utworzony po DELETE
uruchomieniu instrukcji S2 , a zatem nie jest częścią zestawu wierszy widocznych dla niego.
Gdyby PostgreSQL zrestartował całą instrukcję DELETE S2 od początku z nową migawką, działałby tak samo jak SQL Server. Społeczność PostgreSQL nie wybrała tego ze względu na wydajność. W tym prostym przypadku nigdy nie zauważyłbyś różnicy w wydajności, ale gdybyś był dziesięć milionów wierszy w czasie, DELETE
gdy zostałeś zablokowany, na pewno byś to zrobił. Jest tu kompromis, w którym PostgreSQL wybrał wydajność, ponieważ szybsza wersja nadal spełnia wymagania standardu.
S2-2 działa, zgłasza wyjątkowe naruszenie ograniczenia klucza
Oczywiście wiersz już istnieje. To najmniej zaskakująca część obrazu.
Chociaż zachodzi tutaj zaskakujące zachowanie, wszystko jest zgodne ze standardem SQL i mieści się w zakresie tego, co jest „specyficzne dla implementacji” zgodnie ze standardem. Z pewnością może być zaskakujące, jeśli zakładasz, że zachowanie niektórych innych implementacji będzie obecne we wszystkich implementacjach, ale PostgreSQL bardzo stara się uniknąć błędów serializacji na READ COMMITTED
poziomie izolacji i pozwala na niektóre zachowania, które różnią się od innych produktów, aby to osiągnąć.
Teraz osobiście nie jestem wielkim fanem READ COMMITTED
poziomu izolacji transakcji we wdrażaniu jakiegokolwiek produktu. Wszystkie pozwalają warunkom wyścigowym na tworzenie zaskakujących zachowań z transakcyjnego punktu widzenia. Gdy ktoś przyzwyczai się do dziwnych zachowań dozwolonych przez jeden produkt, zwykle uważa to za „normalne”, a kompromisy wybrane przez inny produkt są dziwne. Ale każdy produkt musi mieć jakąś kompromis dla dowolnego trybu, który nie został zaimplementowany jako SERIALIZABLE
. Tam, gdzie programiści PostgreSQL postanowili wprowadzić tę linię, READ COMMITTED
minimalizują blokowanie (odczyty nie blokują zapisów i zapisy nie blokują odczytów) oraz minimalizują ryzyko niepowodzenia serializacji.
Standard wymaga, aby SERIALIZABLE
transakcje były domyślne, ale większość produktów tego nie robi, ponieważ powoduje to spadek wydajności w porównaniu z bardziej swobodnymi poziomami izolacji transakcji. Niektóre produkty nawet nie zapewniają prawdziwie serializowanych transakcji, gdy SERIALIZABLE
zostaną wybrane - w szczególności Oracle i wersje PostgreSQL wcześniejszych niż 9.1. Ale korzystanie z prawdziwych SERIALIZABLE
transakcji jest jedynym sposobem na uniknięcie zaskakujących skutków warunków wyścigu, a SERIALIZABLE
transakcje zawsze muszą albo blokować, aby uniknąć warunków wyścigu, albo wycofać niektóre transakcje, aby uniknąć rozwijających się warunków wyścigu. Najczęstszą implementacją SERIALIZABLE
transakcji jest ścisłe blokowanie dwufazowe (S2PL), które ma zarówno błędy blokowania, jak i serializacji (w postaci zakleszczeń).
Pełne ujawnienie: Współpracowałem z Danem Ports z MIT, aby dodać naprawdę możliwe do serializacji transakcje do PostgreSQL w wersji 9.1 przy użyciu nowej techniki o nazwie Serializable Snapshot Isolation.
READ COMMITTED
transakcji, masz warunek wyścigu: co by się stało, gdyby inna transakcja wstawiła nowy wiersz po pierwszymDELETE
uruchomieniu i przed drugimDELETE
uruchomieniem? W przypadku transakcji mniej rygorystycznych niżSERIALIZABLE
dwa główne sposoby zamknięcia warunków wyścigu są promowanie konfliktu (ale to nie pomaga, gdy wiersz jest usuwany) i materializacja konfliktu. Konflikt można zmaterializować, usuwając tabelę „id”, która była aktualizowana dla każdego wiersza, lub jawnie blokując tabelę. Lub użyj ponownych prób w przypadku błędu.Uważam, że jest to zgodne z projektem, zgodnie z opisem zatwierdzonego do odczytu poziomu izolacji dla PostgreSQL 9.2:
Wiersz włożeniu w
S1
nie istnieje jeszcze kiedyS2
„sDELETE
zaczęło. Nie będzie to widoczne podczas usuwaniaS2
zgodnie z ( 1 ) powyżej. Ten, który zostałS1
usunięty, jest ignorowany przezS2
sDELETE
zgodnie z ( 2 ).W
S2
ten sposób usunięcie nic nie robi. Gdy wkładka przychodzi jednak, że jeden robi zobaczyćS1
„s wkładki:Próba wstawienia przez użytkownika
S2
kończy się niepowodzeniem z naruszeniem ograniczenia.Kontynuowanie czytania tego dokumentu, korzystanie z powtarzalnego odczytu lub nawet serializacji nie rozwiąże całkowicie problemu - druga sesja zakończy się niepowodzeniem z błędem serializacji podczas usuwania.
Pozwoliłoby to jednak ponowić transakcję.
źródło
Całkowicie zgadzam się z doskonałą odpowiedzią @ Mat . Piszę tylko inną odpowiedź, ponieważ nie pasuje do komentarza.
W odpowiedzi na Twój komentarz:
DELETE
w S2 jest już podpięty do określonej wersji wiersza. Ponieważ w międzyczasie zabija go S1, S2 uważa się za udany. Chociaż nie jest to oczywiste na pierwszy rzut oka, seria wydarzeń jest prawie taka:Wszystko zgodnie z projektem. Naprawdę musisz użyć
SERIALIZABLE
transakcji dla swoich wymagań i upewnij się, że spróbujesz ponownie po awarii serializacji.źródło
Użyj DEFERRABLE klucza podstawowego i spróbuj ponownie.
źródło
Napotkaliśmy także ten problem. Nasze rozwiązanie dodaje już
select ... for update
wcześniejdelete from ... where
. Poziom izolacji musi być zatwierdzony do odczytu.źródło