SERIALIZABLE — to najwyższy poziom izolacji transakcji w PostgreSQL. Ten poziom gwarantuje, że wyniki pracy równoległych transakcji będą takie same, jakby były wykonywane PO KOLEI, jedna po drugiej. Żadne anomalia równoległego wykonania (np. Dirty Read, Non-Repeatable Read, Phantom Read) nie mogą się wydarzyć.
Mówiąc prosto, SERIALIZABLE zapewnia pełny porządek i spójność między równoległymi transakcjami. To tak, jakby PostgreSQL mówił: "Wszyscy z transakcjami — do kolejki, panowie!"
Po co nam poziom SERIALIZABLE? Czasem chcesz mieć 100% pewności, że twoje dane zostają całkowicie spójne, mimo równoległych zmian. Wyobraź sobie scenę z supermarketu, gdzie kasjerzy obsługują klientów jednocześnie. Gdyby nikt nie pilnował kolejności, na wyjściu mogłoby być więcej towarów niż zostało kupionych. Z SERIALIZABLE taka sytuacja po prostu nie przejdzie.
Przykład ustawienia poziomu SERIALIZABLE
Żeby ustawić poziom izolacji SERIALIZABLE, użyj komendy:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Na przykład, stwórzmy transakcję, która używa tego poziomu:
BEGIN; -- Startujemy transakcję
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- Ustawiamy poziom izolacji
SELECT * FROM products WHERE category = 'Electronics'; -- Pobieramy listę produktów
UPDATE products SET stock = stock - 1 WHERE product_id = 123; -- Aktualizujemy stan magazynowy
COMMIT; -- Zatwierdzamy zmiany
Case: rezerwacja biletów do kina
Sprawdźmy realny przykład sytuacji, gdzie poziom SERIALIZABLE jest po prostu niezbędny. Wyobraź sobie, że tworzysz system online do rezerwacji biletów do kina. Twoi użytkownicy wybierają miejsca i chcesz mieć pewność, że to samo miejsce nie zostanie kupione przez dwóch klientów jednocześnie.
Najpierw stwórzmy tabelę dla miejsc:
CREATE TABLE seats (
seat_id SERIAL PRIMARY KEY,
is_booked BOOLEAN DEFAULT FALSE
);
Teraz dodajmy kilka miejsc:
INSERT INTO seats (is_booked) VALUES (FALSE), (FALSE), (FALSE);
Pokażmy przykład transakcji z SERIALIZABLE.
Tak można zrobić bezpieczną rezerwację miejsca:
BEGIN; -- Start transakcji
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- Poziom izolacji SERIALIZABLE
-- Sprawdzamy, czy miejsce jest wolne
SELECT is_booked FROM seats WHERE seat_id = 1;
-- Rezerwujemy miejsce
UPDATE seats SET is_booked = TRUE WHERE seat_id = 1;
COMMIT; -- Zatwierdzamy rezerwację
Przy próbie drugiej równoległej transakcji rezerwującej to samo miejsce PostgreSQL nie dopuści do zamieszania i rzuci błąd konfliktu serializacji.
Zapobieganie Phantom Read
Teraz rozkminimy "phantom read", od których tak bardzo chcieliśmy się uwolnić. Phantom Read pojawia się, gdy transakcja widzi zmiany danych dodanych przez inną transakcję w trakcie swojego działania. Na przykład, twoja transakcja oczekuje określonej liczby wierszy, ale nagle inna transakcja dodaje lub usuwa wiersze, zmieniając wyniki.
Zobaczmy przykład:
Dane przed startem transakcji
| id | balance | user |
|---|---|---|
| 1 | 1000 | Alice |
| 2 | 500 | Bob |
Transakcja 1
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Liczymy użytkowników z saldem większym niż 400
SELECT COUNT(*) FROM accounts WHERE balance > 400;
-- Oczekiwany wynik: 2 (Alice i Bob)
Transakcja 2
W innej sesji leci równoległa transakcja:
BEGIN;
INSERT INTO accounts (id, balance, user) VALUES (3, 700, 'Charlie');
COMMIT;
Wracamy do Transakcji 1
-- Powtarzamy zapytanie
SELECT COUNT(*) FROM accounts WHERE balance > 400;
Teraz, jeśli nie używasz SERIALIZABLE, wynik będzie 3 zamiast 2, bo Charlie został dodany w trakcie działania Transakcji 1. To właśnie Phantom Read.
Ale z SERIALIZABLE PostgreSQL gwarantuje, że Transakcja 1 nie zobaczy Charliego, bo jej "widok świata" jest zamrożony w momencie startu transakcji.
Cechy i ograniczenia poziomu SERIALIZABLE
Już wiemy, jak SERIALIZABLE pomaga osiągnąć idealną izolację. Ale co w tym świecie jest idealne bez wad? Pogadajmy szczerze.
Spadek wydajności
SERIALIZABLE wymaga dużo więcej zasobów niż poziomy READ COMMITTED czy REPEATABLE READ. Dlaczego? PostgreSQL musi emulować sekwencyjne wykonanie operacji, śledząc wszystkie możliwe konflikty między transakcjami.
Błędy serializacji
Jeśli PostgreSQL wykryje, że nie da się wykonać transakcji w "idealnej kolejności", generuje błąd serializacji (serialization_failure) i cofa transakcję.
Przykład błędu:
ERROR: could not serialize access due to concurrent update
Żeby sobie z tym radzić, możemy ponownie uruchomić transakcję po niepowodzeniu:
DO $$
DECLARE
done BOOLEAN := FALSE;
BEGIN
WHILE NOT done LOOP
BEGIN
-- Startujemy transakcję
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Wykonujemy operacje
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- Zatwierdzamy zmiany
COMMIT;
done := TRUE; -- Wychodzimy z pętli, jeśli wszystko OK
EXCEPTION WHEN serialization_failure THEN
ROLLBACK; -- Cofamy przy błędzie
END;
END LOOP;
END;
$$;
To standardowe podejście w systemach, gdzie używasz SERIALIZABLE.
Ten kod jest napisany w PL-SQL. Wrócimy do niego później. Po prostu chciałem pokazać ci ładny i działający kod. No i pokazać, po co jest PL-SQL :)
Kiedy używać SERIALIZABLE?
Ten poziom izolacji ma sens tam, gdzie cena błędu jest bardzo wysoka:
- Transakcje finansowe, jak obsługa płatności czy rozdzielanie bonusów.
- Systemy zarządzania zapasami, żeby uniknąć podwójnych zamówień.
- Rezerwacje online, gdzie ważne jest wykluczenie konfliktu przy rezerwacji zasobów.
Jeśli tworzysz system, gdzie dane muszą być w 100% spójne, a wydajność schodzi na drugi plan, SERIALIZABLE będzie twoim najlepszym kumplem.
GO TO FULL VERSION