Transakcje, poziomy izolacji i blokady

Zarządzanie transakcjami


Czym są transakcje? Przytoczę tutaj sztampowy przykład przelewów w banku. Wyobraźmy sobie taką sytuację – jeden klient przelewa pewną kwotę drugiemu. System już zdążył pobrać kwotę z konta przelewającego, po czym następuje awaria. Kwota nie pojawia się na koncie beneficjenta. Czy może to tak działać? Dla banku pewnie byłaby to korzystna sytuacja, trochę mniej dla klientów. Obie operacje – pomniejszenia stanu konta jednego klienta i powiększenia innego muszą wykonać się razem, albo żadna z nich. Do tego celu potrzebujemy właśnie transakcji. Osoby pracujące dotąd z bazami Oracle mogą mieć przyzwyczajenie że każda operacja DML rozpoczyna transakcję, a kończy ją jawny bądź niejawny COMMIT lub ROLLBACK. W systemach Oracle domyślnie do czasu zatwierdzenia transakcji, inne sesje nie widzą zmian w bazie. Tu jest podobnie, z tą różnicą że domyślnie w bazach PostgreSQL włączone jest zatwierdzanie. Wykonując więc operację DML musimy się liczyć z tym że zostanie ona od razu zatwierdzona. Jeśli chcemy rozpocząć transakcję która nie zostanie zatwierdzona tak długo jak długo nie zrobimy tego jawnie, wydajemy polecenie:


begin work;


bądź po prostu :


begin;


Słówko „work” nie jest obligatoryjne. Aby zatwierdzić transakcję wykonaj komendę :


commit work;


lub


commit;


Aby ją wycofać :


rollback work;


lub


rollback;


Zatwierdzenie lub wycofanie transakcji będzie dotyczyć wszystkich operacji w ramach niej.

Jeszcze słówko do użytkowników Oracle – mogliście przywyknąć do tego, że operacje DDL wysyłają niejawny commit, tutaj jest to postawione na głowie. Operacje DDL są objęte transakcjami , czyli jeśli jawnie rozpoczniesz transakcję, stworzysz tabelę a następnie transakcję wycofasz – zostanie wycofane stworzenie tabeli O.o (!!!!) Aby się o tym przekonać wykonaj poniższe polecenia:

begin work;

create table public.tab (x integer);

insert into public.tab values (0);

select * from public.tab;

rollback work;

select * from public.tab;


Niepożądane zjawiska związane z transakcyjnością


Do takich zjawisk zaliczyć możemy :


  • Brudny odczyt

  • Odczyt nie dający się powtórzyć

  • Odczyty widmo

  • Utracone aktualizacje


Brudny odczyt


Domyślnie inne sesje nie będą widziały zmian które wykonami a nie zatwierdzimy (jawnie lub nie). Gdyby mogły – mielibyśmy do czynienia z brudnym odczytem. PostgreSQL nie umożliwia operacji brudnego odczytu.


Odczyty nie dające się powtórzyć


Domyślnie jeśli zatwierdzisz (jawnie lub nie) jakąś operację, inne sesje będą widziały Twoje zmiany zmiany. Nie jest to zależne od tego czy inna sesja ma w tej chwili trwającą transakcję czy nie. Nazywamy to odczytem nie dającym się powtórzyć, PostgreSQL domyślnie pozwala na tego typu transakcje. Gdyby taki odczyt był zabroniony, inne sesje widziałyby zmiany dopiero po zakończeniu swoich transakcji – o ile oczywiście mają jakieś trwające.


Odczyty widmo


Wyobraź sobie taką sekwencję czynności:

  • Sesje A i B rozpoczynają swoje transakcję

  • Sesja A wykonuje UPDATE na wszystkich wierszach tabeli PRODUKTY

  • Sesja B dodaje nowy wiersz do tabeli PRODUKTY

  • Sesje A i B zatwierdzają swoje transakcje.

Czy zmiana wykonana przez sesję A będzie dotyczyła również wiersza dodanego przez sesję B? Nie! Dzieje się tak ponieważ  w momencie określania przez sesję A które wiersze należy zaktualizować, nowo dodany przez sesję A wiersz dla sesji B jeszcze nie istnieje. Sesja A zobaczy nowy wierz dopiero po zatwierdzeniu transakcji przez sesję B.

Poziomy izolacji


To jakie zjawiska będziemy obserwować zależy od poziomu izolacji jaki będziemy mięli ustawiony poziom izolacji. Domyślnym trybem jest READ COMMITED. Mamy do wyboru dwa – READ COMMITED i SERIALIZABLE (tak jak w Oracle). Różnica polega na tym, że w trybie SERIALIZABLE nie będziemy mięli do czynienia ze zjawiskiem odczytu nie dającego się powtórzyć ani odczytami widmo. Możemy to zmienić za pomocą komendy:


set transaction isolation level read commited / serializable;


Blokady

Zasada działania blokad



Istnieją dwa tryby blokad :

  • blokada współdzielona - taka blokada pozwala innym sesjom odczytywać, ale nie pozwala na zmianę zablokowanych danych

  • blokada wyłączna – nie pozwala ani czytać ani zmieniać zablokowanych danych


Domyślnie w PostgreSQL działa blokada współdzielona. Jeśli jakaś sesja rozpocznie transakcję i zmieni dane, inne sesje do czasu zatwierdzenia owej transakcji będą mogły odczytywać oryginalną postać danych, ale nie będą mogły tych danych zmieniać.

Wiersze i tabele mogą być blokowane tylko w ramach transakcji. Po jej zakończeniu są natychmiast odblokowywane.

Może dojść do sytuacji zakleszczenia. Przykładowo sesje A i B rozpoczynają transakcję. Sesja A zmienia wiersze w tabeli X, sesja B zmienia wiersze w tabeli Y. Następnie sesja A usiłuje zmieniać wiersze zablokowane przez sesje B w tabeli Y, a sesja B usiłuje zmieniać wiersze zablokowane przez sesję A w tabeli X. Taki pat nazywamy zakleszczeniem. Obie sesje blokują się wzajemnie. Po pewnym czasie w jednej z sesji dostaniemy komunikat „Deadlock detected”. Nie jesteśmy w stanie przewidzieć w której, dokumentacja milczy na ten temat.

Jawne blokowanie wierszy i tabel


W celu uniknięcia zakleszczeń, lub aby w „międzyczasie” możemy jawnie zablokować wiersze lub tabelę. Dzięki temu będziemy mieli pewność że zmiany wprowadzane przez inne sesje nie będą kolidowały z naszymi.

Wiersze możemy zablokować komendą np. :


select * from produkty where cena>500 for update;


Zadziała to jednak tylko i wyłącznie jeśli będziemy mieli rozpoczętą transakcję! Wiersze pozostaną zablokowane do czasu zakończenia naszej transakcji.


Możesz też zablokować całą tabelę:


lock table produkty;


Tutaj mamy jednak aż dwa obostrzenia. Po pierwsze działa to tylko w ramach transakcji, po drugie blokuje wszelki dostęp do tabeli – w tym odczyt!


Mechanizmy wewnętrzne transakcyjności i operacja VACUUM


Ktoś dociekliwy mógłby się zacząć zastanawiać jak to w ogóle działa? Jak to jest możliwe że dwie sesje widzą zupełnie różne rzeczy w sytuacji gdy jedna z nich zmieni dane w ramach transakcji ale nie zatwierdzi? Gdy zmieniasz jakiś wiersz, PostgreSQL tworzy nową kopię wiersza na której Ty jako zmieniający operujesz. Kopia tego wiersza znajduje się w ramach tabeli w której oryginalny wiersz się znajduje. Dla zapytań odpytujących tę tabelę dostępne są stare wiersze. Po zakończeniu transakcji wszystkie zapytania wszystkich transakcji korzystają już z nowych wierszy. Taki sposób działania sprawia, że w plikach związanych z tabelami z czasem powstaje ogromna ilość nieużywanych wierszy wierszy do których już nawet nie ma dostępu. To z kolei powoduje rozrost plików danych. Miejsce zajmowane przez takie wiersze można odzyskać za pomocą polecenia VACUUM. Polecenie to poza odzyskiwaniem wiersza może też odświeżać statystyki – ale nie jest to tematem tego rozdziału.

Sprawdźmy więc działanie tego polecenia. Wykonaj sekwencję poniższych komend aby stworzyć przykładową tabelę i zapełnić ją danymi.



create table wielka (x integer,y varchar);

do

$$

begin

for x in 1..1000000 loop

INSERT INTO WIELKA VALUES (X,'X='||X);

end loop;

end;

$$;


VACUUM VERBOSE WIELKA;


Na końcu wywoływany jest vacuum który powinien nam zwrócić mniej więcej taki wynik:




Jak widać mamy milion nieusuwalnych wierszy – to są te przed momentem wstawione, oraz 0 usuwalnych. Nie dokonaliśmy żadnych zmian jak dotąd, więc po prostu nie ma niepotrzebnych wierszy które można usunąć. Zmieńmy więc dane tak by takie wiersze powstały i ponownie przeprowadźmy czyszczenie:

UPDATE WIELKA SET Y='COS TAKIEGO';

VACUUM VERBOSE WIELKA;




Tym razem już było co usunąć, co też zostało wykonane. Zwolniło się sporo miejsca na nowe dane. Możesz też wykonać czyszczenie dla całej bazy danych:


vacuum verbose;

Ten artykuł jest elementem poniższych kursów: