W dzisiejszym wpisie chciałbym poruszyć temat dotyczący transakcji, a dokładnie ich izolacji. Z transakcjami możemy się spotkać praktycznie w każdym systemie dotykającym baz danych. Czym one są? Zbiorem operacji, które stanowią pewną całość. Muszą one zostać wykonane wszystkie razem. Natomiast gdy wystąpi błąd podczas wykonywania danej transakcji to wszystkie zmiany w niej zawarte zostają wycofane. Nie ma mowy o żadnych półśrodkach. Jednym z przykładów może być mechanizm płatności za wybrane produkty. Albo obciążamy nasz rachunek i otrzymujemy towar albo nic się nie dzieje. Przy okazji zapraszam Cię do artykułu na temat adnotacji @Transactional w Springu i jej propagacji.

Co, gdy wiele transakcji występuje na raz?

W idealnym świecie, gdzie występowałaby w systemie tylko jedna transakcja na raz, nie byłoby żadnych problemów. Natomiast oczywiście taka kolej rzeczy nie ma miejsca. Jeżeli wielu użytkowników korzysta z naszej aplikacji to niemalże ze 100% pewnością można powiedzieć, że rozpoczęte przez nich transakcje dotkną tych samych zasobów w bazie danych w danym momencie. Wtedy mogą się pojawić czyhające na nas wyzwania związane z współzawodnictwem transakcji.

Dirty read

Poziom izolacji ‘READ UNCOMMITTED’ pozwala nam na odczyt zmian, które zaszły w jeszcze nie zacommitowanej transakcji. To podejście może przysparzać pewien problem. Wracając do sytuacji z produktem oraz administratorami. Pierwszy administrator postanawia zaktualizować cenę produktu. Pobiera więc w swojej transakcji rekord odpowiadający wybranemu produktowi o wartości 80zł. Zamienia kwotę na 100zł, ale jeszcze nie kończy swojej transakcji. W tym momencie drugi administrator robi tzw. “brudny odczyt” i zamiast otrzymać 80zł jako wartość produktu w jego transakcji będzie ona wynosiła 100zł. Na jej podstawie, w tej samej transakcji, dokonuje pewnych zmian w systemie np. naliczając komuś rabat z racji przekroczenia danej sumy zakupów. Jednak wtedy transakcja pierwszego administratora wycofuje zmiany z powodu błędu i produkt wraca do swojej pierwotnej ceny 80zł. Drugi administrator także kończy swoją pracę, ale z pozytywnym rezultatem. Tutaj następuję nieprawidłowość w procesie biznesowym, ponieważ rabat nie powinien zostać naliczony przy wartości produktu wynoszącej 80zł, która obecnie znajduje się w bazie danych.

Przykład błędu izolacji transakcji - Dirty Read
Przykład błędu izolacji transakcji - Dirty Read

Unrepeatable read

Załóżmy, że w danym procesie biznesowym należy w tej samej transakcji dwa razy sięgnąć do bazy danych po cenę produktu. Jednak pomiędzy tymi dwoma zapytaniami jeden z administratorów postanawia zaktualizować tą wartość commitując swoją transakcję. Wtedy nasz proces biznesowy będzie operował na dwóch różnych kwotach tego samego produktu co może przynieść bardzo nieprzewidywalne, a często i niekorzystne skutki.

Przykład błędu izolacji transakcji - Unrepeatable read
Przykład błędu izolacji transakcji - Unrepeatable read

Phantom read

Phantom read jest dosyć podobny do Unrepeatable read. Jednak w jego przypadku problemem jest zmienna liczba pobieranych wierszy w tej samej transakcji. Wyobraźmy sobie, że w pewnym procesie biznesowym potrzebne jest dwukrotne uzyskanie listy produktów. Natomiast inna funkcja biznesowa ma możliwość dodania kolejnych pozycji. Phantom read pojawia się wtedy, gdy obydwa procesy zajdą w tym samym czasie. Na start pierwsza transakcja pobiera listę artykułów po raz pierwszy. Po tym zdarzeniu pracę rozpoczyna druga transakcja, która dodaje produkt do bazy danych i kończy swoje działanie. Wtedy do głosu dochodzi pierwsza transakcja, która ponownie uzyskuje listę produktów. Jednak w tym momencie jej skład uwzględnia już dodany element przez drugą transakcję. Następuje odczyt widmo, który może powodować problemy w danym procesie biznesowym. Ten proces zakładał, że w trakcie jego działania będzie posiadał zawsze tą samą liczbę artykułów co niestety nie jest spełnione.

Przykład błędu izolacji transakcji - Phantom read
Przykład błędu izolacji transakcji - Phantom read

Lost update

Ostatnim problemem jest wyścig pomiędzy dwoma transakcjami, które aktualizują ten sam wiersz. Jest on jednak dosyć specyficzny, ponieważ zwycięzcą jest ten kto dotrze na metę ostatni. Jako zobrazowanie załóżmy, że dwóch administratorów chce zmienić cenę tego samego produktu. Pobierają ją oni, aktualizują i zapisują w bazie. Pierwszy chce podnieść cenę o 25% od aktualnej ceny, natomiast drugi prawie dwukrotnie. Jeśli jednak zrobią to w tym samym czasie to ostatni z administratorów nadpisze pracę tego pierwszego bez jej uwzględnienia (dla niego ta zmiana w ogóle nie zaistniała). W większości przypadków taka sytuacja nie będzie miała negatywnych skutków. Jednak czasami nowa wartość może być wyliczana na podstawie poprzedniej, tak jak u nas, przez co krytyczność tego problemu może znacznie wzrosnąć.

Przykład błędu izolacji transakcji - Last update wins
Przykład błędu izolacji transakcji - Lost update

Podsumowanie

Mam nadzieję, że problemy związane z izolacjami transakcji są dla Ciebie jasne. Oczywiście niektóre z nich mogą wystąpić tylko przy konkretnym poziomie izolacji. Im jest on bardziej restrykcyjny tym jest mniej błędogenny, ale również cechuje się mniejszą dostępnością. Do dyspozycji mamy: SERIALIZABLE, REPEATABLE READ, READ COMMITTED oraz READ UNCOMMITTED. Na tej stronie znajdziesz ich krótki opis.