Wyobraź sobie, że grasz w grę online, a w tym czasie jakiś cheater grzebie w jej kodzie i wzmacnia swoją postać... albo czytasz książkę w bibliotece, a ktoś w tym czasie może zmieniać strony, wstawiać nowe rozdziały albo nawet podmienić całą książkę. Słaba akcja, nie? No właśnie przed takimi "niespodziankami" chroni poziom izolacji REPEATABLE READ.
REPEATABLE READ daje ci gwarancję, że dane, które widzisz w ramach jednej transakcji, nie zmienią się do końca tej transakcji. Nawet jeśli inna transakcja spróbuje zaktualizować te dane, twoja transakcja będzie odporna na takie zmiany.
Kluczowe cechy:
- Zapobiega
Dirty Read(czyli czytaniu danych, które nie zostały jeszcze zatwierdzone). - I co najważniejsze, zapobiega
Non-Repeatable Read. To znaczy, że jeśli przeczytasz jakiś zestaw danych na początku transakcji, to przy ponownym odczycie dostaniesz dokładnie te same dane, nawet jeśli ktoś inny je zmienił.
Jednak REPEATABLE READ nie chroni przed Phantom Read. Jeśli inna transakcja doda nowe wiersze, mogą się one pojawić w twoim kolejnym zapytaniu. Żeby pozbyć się też tej anomalii, potrzebny będzie poziom SERIALIZABLE, ale o tym pogadamy później.
Jak ustawić poziom izolacji REPEATABLE READ
Zanim przejdziemy do przykładów, zobaczmy, jak włączyć ten poziom izolacji w PostgreSQL. Są dwa główne sposoby:
Ustawić poziom izolacji dla konkretnej transakcji:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; -- Twoje zapytania COMMIT;Ustawić poziom izolacji dla bieżącej sesji:
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
W drugim przypadku wszystkie transakcje w tej sesji będą używać REPEATABLE READ.
Przykład: zapobieganie Non-Repeatable Read
Załóżmy, że mamy tabelę accounts o takiej strukturze:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'oczekujące'
);
INSERT INTO orders (customer_name, status)
VALUES ('Alice', 'oczekujące'), ('Bob', 'oczekujące');
Zacznijmy od podstawowego scenariusza, gdzie jedna transakcja zmienia dane, a druga je czyta.
Scenariusz bez REPEATABLE READ (poziom READ COMMITTED)
Transakcja 1 zaczyna pracę:
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- Dostajemy: 100
W tym czasie Transakcja 2 zmienia dane:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
Transakcja 1 kontynuuje:
SELECT balance FROM accounts WHERE account_id = 1;
-- Dostajemy: 150 (dane się zmieniły!)
COMMIT;
Jak widzisz, przy poziomie READ COMMITTED dane mogą się zmienić między dwoma odczytami w ramach jednej transakcji. To właśnie jest Non-Repeatable Read.
Scenariusz z REPEATABLE READ
Teraz spróbujmy tego samego przykładu, ale z poziomem izolacji REPEATABLE READ.
Transakcja 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- Dostajemy: 100
Transakcja 2:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
Transakcja 1 kontynuuje pracę:
SELECT balance FROM accounts WHERE account_id = 1;
-- Nadal dostajemy: 100 (dane niezmienne!)
COMMIT;
Niezależnie od zmian wprowadzonych przez inną transakcję, transakcja 1 widzi dane takie, jakie były na początku. Dzięki temu Non-Repeatable Read jest wyeliminowane.
Jak działa REPEATABLE READ
PostgreSQL używa mechanizmu MVCC (Multi-Version Concurrency Control), żeby zaimplementować poziom izolacji REPEATABLE READ. Główna zasada MVCC to to, że każda transakcja dostaje stabilny "snapshot" bazy, który nie zmienia się do końca transakcji. Osiąga się to przez tworzenie i zarządzanie wieloma wersjami wierszy.
Kiedy transakcja startuje, widzi dane w takim stanie, w jakim były na początku jej działania. Jeśli inna transakcja coś zmieni, PostgreSQL tworzy nową wersję wiersza, ale poprzednia wersja zostaje dla wszystkich transakcji, które jej używają.
Właśnie dlatego transakcje działają wolniej i potrzebują więcej pamięci. I właśnie dlatego mało kto używa najmocniejszego poziomu izolacji: jest najbezpieczniejszy, ale najbardziej spowalnia pracę z bazą.
Ograniczenia REPEATABLE READ: Phantom Read
Jak już wspomnieliśmy, REPEATABLE READ nie chroni przed Phantom Read. Żeby zrozumieć, o co chodzi, zobaczmy przykład z zapytaniami, które operują na zakresach danych.
Załóżmy, że mamy tabelę orders:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
amount NUMERIC NOT NULL
);
INSERT INTO orders (amount)
VALUES (50), (100), (150);
Transakcja 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- Dostajemy: 2
Transakcja 2:
BEGIN;
INSERT INTO orders (amount) VALUES (200);
COMMIT;
Transakcja 1 kontynuuje pracę:
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- Dostajemy: 3 (nowy wiersz pojawił się w wyniku zapytania!)
COMMIT;
W tym przypadku nowy wiersz (z amount = 200) został dodany przez inną transakcję i "widmowo" pojawił się w wyniku zapytania transakcji 1, mimo poziomu izolacji REPEATABLE READ.
Jeśli chcesz uniknąć Phantom Read, musisz użyć poziomu SERIALIZABLE, ale to zawsze oznacza kompromis z wydajnością.
Zalety i wady REPEATABLE READ
Poziom izolacji REPEATABLE READ — świetne rozwiązanie, gdy potrzebujesz pewności, że dane nie zmienią się w trakcie trwania transakcji. Jak tylko coś przeczytasz, ta wartość zostanie taka sama aż do COMMIT, nawet jeśli ktoś w innej transakcji spróbuje coś zmienić.
Takie podejście zapobiega zarówno brudnym odczytom (dirty read), jak i niepowtarzalnym odczytom (non-repeatable read). Pracujesz z tymi samymi danymi, co na początku — żadnych niespodziewanych zmian "w locie". To szczególnie przydatne, gdy generujesz raporty albo podejmujesz decyzje, gdzie ważna jest spójność.
Z drugiej strony, REPEATABLE READ nie radzi sobie z tzw. "fantomami" (phantom read) — kiedy nowe wiersze pojawiają się w wyniku zapytania, które już wykonałeś w tej samej transakcji. Poza tym, przy dużym obciążeniu taki poziom może powodować konflikty między transakcjami, zwłaszcza jeśli często korzystają z tych samych danych. To może prowadzić do blokad i rollbacków, nawet jeśli zapytania były poprawne.
Ogólnie, REPEATABLE READ to dobry kompromis między niezawodnością a wydajnością, ale w scenariuszach z dużą konkurencją może wymagać dodatkowych ustawień i uwagi.
Przydatne wskazówki i typowe błędy
- Pamiętaj, że wybór poziomu izolacji wpływa na wydajność. Używaj
REPEATABLE READtylko wtedy, gdy naprawdę potrzebujesz pewności niezmienności danych. - Pomyłka między
REPEATABLE READaSERIALIZABLEto częsty błąd. Jeśli widzisz nowe wiersze w wyniku ponownego zapytania, to normalne zachowanie dlaREPEATABLE READ. - Przy pracy z długimi transakcjami uważaj na możliwe konflikty blokad. Długie transakcje mogą zablokować inne operacje.
PostgreSQL daje ci różne narzędzia do zarządzania izolacją transakcji. Poziom REPEATABLE READ jest idealny, gdy zależy ci na pewności, że już przeczytane dane nie zmienią się w ramach jednej transakcji.
GO TO FULL VERSION