PostgreSQL wdraża Serializable Snapshot Isolation (SSI) używając blokad predykatów i testowania grafów serializacji, aby osiągnąć prawdziwą serializowalność bez kar wydajnościowych tradycyjnych dwuetapowych blokad. Błąd 40001 (serialization_failure) występuje szczególnie podczas write skew lub read-write conflicts, gdzie dwie transakcje ustanawiają cykl rw-zależności. Na przykład, Transakcja A odczytuje wiersze spełniające predykat (np. WHERE color = 'red'), Transakcja B odczytuje wiersze spełniające nieprzechodzący predykat (np. WHERE color = 'blue'), a następnie A aktualizuje wiersze na 'blue', podczas gdy B aktualizuje wiersze na 'red'. Żadna transakcja nie blokuje drugiej, ale wynik jest nieserializowalny.
Ten wzór reprezentuje niebezpieczną strukturę w grafie serializacji: dwa kolejno po sobie rw-antyzależności tworzące potencjalny cykl. PostgreSQL wykrywa to i przerywa jedną transakcję, aby zapobiec anomalnym stanom. Problem jest subtelny, ponieważ transakcje mogą modyfikować różne fizyczne wiersze, co sprawia, że konflikt jest niewidoczny dla mechanizmów blokowania wierszy używanych w niższych poziomach izolacji.
Wymagana jest implementacja optymistycznej pętli ponownego wykonania na poziomie aplikacji. Po złapaniu WYJĄTKU SQL '40001', aplikacja musi wycofać bieżącą transakcję i ponownie uruchomić całą operację z wykorzystaniem zwiększającego się czasu oczekiwania. W przeciwieństwie do martwych blokad, które zazwyczaj rozwiązuje się natychmiastowym ponownym wykonaniem, niepowodzenia serializacji w warunkach wysokiego obciążenia korzystają z opóźnień z jitter'em, aby zapobiec „tłumieniu stada”.
-- Przykład logiki ponownego wykonania aplikacji w PL/pgSQL DO $$ DECLARE retries INT := 0; max_retries INT := 3; BEGIN WHILE retries < max_retries LOOP BEGIN SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE; PERFORM * FROM inventory WHERE category = 'electronics' AND count > 0; UPDATE inventory SET count = count - 1 WHERE item_id = 123; COMMIT; EXIT; EXCEPTION WHEN SQLSTATE '40001' THEN ROLLBACK; retries := retries + 1; PERFORM pg_sleep(power(2, retries) * 0.1); -- Zwiększający się czas oczekiwania END; END LOOP; END $$;
Platforma wymiany biletów na koncerty pozwalała użytkownikom wymieniać kategorie miejsc za pomocą logiki sprawdzenia-a-potem-działania. Transakcja A zweryfikowała, że miejsca VIP były dostępne, a następnie obniżyła zarezerwowane miejsce VIP do standardowego. Jednocześnie Transakcja B zweryfikowała dostępność standardowych miejsc i podniosła standardowe miejsce do VIP. Podczas READ COMMITTED, obie transakcje odczytały dostępność jako prawdziwą, wykonały aktualizacje, a system zakończył z ujemnym stanem zapasów w obu kategoriach, mimo że każda transakcja sprawdzała ograniczenia.
Zaprojektowano trzy rozwiązania. Pierwsze używało jawnych blokad SELECT FOR UPDATE, ale to nie powiodło się, gdy zapytania o dostępność zwróciły zero wierszy, co spowodowało, że nie uzyskano blokad i pozostawiono system podatny na phantom inserts. Drugie podejście wdrożyło ADVISORY LOCKS używając pg_try_advisory_lock() do seryjnego dostępu do kategorii miejsc, co zapobiegało konfliktom, ale wprowadzało złożone ryzyko porządku blokady i zmniejszało przezroczystość o 40% z powodu serializacji wszystkich kontroli kategorii.
Trzecie rozwiązanie przyjęło izolację SERIALIZABLE z pętlą ponownego wykonania na poziomie aplikacji. To zostało wybrane, ponieważ gwarantowało poprawność bez zarządzania ręcznym blokowaniem, a koszty ponownego wykonania były akceptowalne, biorąc pod uwagę niską częstotliwość jednoczesnych wymian w porównaniu do operacji odczytu. Implementacja używała dostosowywania ponownego wykonania JDBC łapiąc SQLException z SQLState 40001, czekając 100ms * 2^nowa_próba, i ponownie wykonując transakcję. Zlikwidowało to całkowicie incydenty związane z nadmiernymi rezerwacjami, chociaż czas opóźnienia p99 wzrósł o 15ms podczas szczytowych okien sprzedaży.
Jaka jest precyzyjna różnica między blokadami predykatów w izolacji serializowanej a blokadami wierszy w powtarzalnym odczycie?
Powtarzalny odczyt zapobiega niepowtarzalnym odczytom poprzez blokowanie wierszy rzeczywiście zwróconych przez zapytanie, ale nie zapobiega phantom read—nowym wierszom wstawionym przez inne transakcje, które spełniają klauzulę WHERE zapytania. Izolacja serializowana używa blokad predykatów, które blokują sam zakres wyszukiwania, zapobiegając jakiejkolwiek wstawianiu, które pasowałoby do predykatu zapytania, nawet w wierszach, które nie istniały, gdy zapytanie zostało wykonane. Kandydaci często mylą te dwie rzeczy, błędnie uważając, że Powtarzalny odczyt zapobiega phantom read lub że Izolowany blokuje tylko istniejące wiersze.
Jak algorytm testowania grafów serializacji ustala, którą transakcję przerwać, gdy wykryty zostaje cykl?
PostgreSQL stosuje strategię „pierwszy zatwierdzający wygrywa” w połączeniu z wykrywaniem niebezpiecznych struktur. Gdy między równoległymi transakcjami tworzy się rw-konflikt (zależność od odczytu-do-zapisu), system śledzi, czy ta krawędź zamyka cykl w grafie serializacji. Transakcja, która zamyka cykl, jest przerywana z SQLSTATE 40001. Wybór jest deterministyczny w oparciu o strukturę grafu, a nie wiek transakcji, faworyzując przerywanie transakcji, których wycofanie jest najmniej kosztowne lub najnowsze w wykrytym cyklu. Zrozumienie, że jest to prewencyjne przerywanie (zapobiegające nieważnej historii) raczej niż martwa blokada (czekanie na blokady), jest kluczowe dla prawidłowego zarządzania błędami.
Dlaczego SELECT FOR UPDATE może nie zapobiec niepowodzeniom serializacji w scenariuszach, gdzie izolacja serializowana wykrywa konflikt?
SELECT FOR UPDATE uzyskuje ROW SHARE blokady tylko na wierszach, które istnieją w momencie wykonania. W wzorach sprawdzania-a-potem-działania, gdy wstępne zapytanie zwraca zero wierszy (np. sprawdzanie dostępności zerowych miejsc), FOR UPDATE nie uzyskuje żadnych blokad, co pozwala innej transakcji wstawić konfliktujący wiersz. Izolacja serializowana wykrywa to jako konflikt predykatu, ponieważ wynik „zero wierszy” stanowi ważny zbiór odczytu, który został unieważniony przez równoległe wstawienie. Kandydaci często błędnie zakładają, że FOR UPDATE zapewnia kompleksową ochronę, nie zdając sobie sprawy, że nie oferuje żadnej obrony przed phantom inserts, gdy predykat początkowo nie pasuje do żadnego wiersza.