Transakcje oraz poziomy izolacji w Entity Framework i SQL Server

Czołem,

jeśli coś mogę powiedzieć o Wojskowej Akademii Technicznej to fakt, że poziom wymagany od studentów na przedmiocie Bazy danych był wysoki. Pisanie zapytań SQL na kartce z kilkoma klauzulami join, group by, select itd. to był dla mnie niepojęty kosmos. Sporo pojawiło się także teorii, którą później w mniej lub bardziej zgrabny sposób przytoczę. Kiedyś jednak natrafiłem na artykuł, który zahaczał o temat poziomów izolacji (ang. isolation level). Co to jest? Czy gdzieś to wykorzystujemy? A może korzystamy z tego całkowicie nieświadomie zamknięci w ORM-owej bańce. Dziś wszelkie wątpliwości zostaną rozmyte 🙂

Żeby mówić o poziomach izolacji musimy najpierw poznać definicję transakcji bazodanowej. Pamiętam do dziś jak wpajał nam to jeden z wykładowców WAT-u:

 

Transakcja to przejście bazy danych ze stanu spójnego w spójny.

 

Jest to pojęcie może lekko ogólne i nie oddaje wszystkich istotnych aspektów, ale jako taki programistyczny punch line działa świetnie. Rozwińmy zatem nieco temat. Każda transakcja powinna spełniać cztery zasady określone jako ACID:

  • Atomicity (pol. atomowość) – każda transakcja może wykonać się zero-jedynkowo. Albo się uda, albo nie. Żadne stany pośrednie nie są dozwolone.
  • Consistency (pol. spójność)  po zakończeniu transakcji baza danych powinna pozostać w stanie spójnym. Jeżeli ktoś jest zainteresowany definicją spójności to odsyłam do fajnego wpisu, który znalazłem o tu.
  • Isolation (pol. izolacja) – o tym za chwilę 😉
  • Durability (pol. trwałość) – po wszelkiego rodzaju awariach, przerwach dostępność, wszelkie dane modyfikowane w ramach zakończonych transakcji powinny być nienaruszone.

Wiemy już zatem czym transakcja jest i czym powinna się charakteryzować. Przejdźmy na chwilę do Entity Framework. Tak naprawdę części z Was mogłaby nasunąć się pewna refleksja: „Hmmm korzystam z EF i jakoś nie kojarzę tworzenia takich rzeczy, odpalam metodę SaveChanges() i działa!”. To oczywiście prawda. Entity Framework domyślnie opakowuje zapis naszych danych w transakcję, która trwa tylko niezbędne minimum, żeby oszczędzić zasoby. Co więcej EF jest na tyle mądry, że transakcje nie są nakładane na IQueryable, a to dlatego że query z definicji nie zmieniają stanu bazy danych. Czasami jednak warto zainteresować się własnym zarządzaniem wszelkich operacji bazodanowych. Jest to przydatne chociażby w implementacji wzorca Unit Of Work, który opisywałem jakiś czas temu na blogu. I właśnie listing z tamtego wpisu po części przytoczę:

 


private readonly IRelationalTransaction _transaction;


public UnitOfWork(AuroraContext context)
{
     _context = context;
     _transaction = context.Database.BeginTransaction();
}


public async Task<int> CommitAsync()
{
     // logika
     _transaction.Commit();             
     //logika        
}

 

W konstruktorze przedstawiłem sposób w jaki możemy naszą transakcję rozpocząć. Robimy to poprzez wywołanie metody BeginTransaction na naszym kontekście. Od tego momentu nasza transakcja jest w toku. Mamy teraz dwie możliwości:

  • commit – zapisanie zmian poprzez naszą transakcję (przykład w metodzie CommitAsync )
  • rollback – jest to tzw. wycofanie transakcji. Po rollbacku postać naszej bazy danych będzie identyczna jak przed rozpoczęciem transakcji.

Po każdej z tych operacji zalecane jest pozbycie się transakcji poprzez wywołanie metody Dispose. Naturalnym ruchem jest zatem umieszczenie obiektu transakcyjnego w klauzuli using, żeby o tym nie zapomnieć 😉 Jeszcze krótkie dopowiedzenie do metody BeginTransaction. Jeżeli przyjrzymy się uważniej, zauważymy że posiada ona przeciążenie:

visual

 

Jak widać możemy w tym miejscu zadecydować o poziomie izolacji, który zostanie wykorzystany. Czas więc w końcu powiedzieć sobie co to tak naprawdę jest, ale zanim to nastąpi musimy uściślić sobie jeszcze jedną kwestię (już ostatnią 😉 ). W SQL uruchamianie równoległe, kliku operacji (transakcji) może prowadzić do kilku nieprzewidywalnych anomalii. Jest to:

  • dirty read
  • lost update
  • nonrepeatable read
  • phantom read

Omówmy na przykładach co to jest.

 

example_1

 

O dirty read mówimy wtedy, gdy dane pobrane w jednej transakcji nie zostały zatwierdzone przez drugą. Mamy taką oto sytuację. W banku X użytkownik 1 ma na swoim koncie 20 zł, a użytkownik 2 ma 100zł. Następnie pan numer jeden postanawia wykonać drugiemu przelew na kwotę 30 zł. Aby tego dokonać musimy wykonać dwie czynności. Po pierwsze zwiększyć użytkownikowi 2 saldo, po drugie odjąć pieniądze z konta użytkownika 1. Pierwsza faza przechodzi bezproblemowo. W drugiej części okazuje się jednak, że środki na koncie delikwenta są niewystarczające do realizacji operacji. Całą transakcję musimy wycofać, aby baza danych była spójna. Co w przypadku, gdy w momencie nieprawidłowości ktoś postanowi wyświetlić saldo użytkownika 2? Rollback jeszcze nie nastąpił, a kwota na jego koncie póki co jest powiększona o kwotę przelewu. Wobec tego pobrane dane nie są zgodne z rzeczywistością.

 

example_2

Pojęcie lost update, sugeruje że tracimy wartość aktualizowaną na bazie danych. Pozostając w temacie bankowości wyobraźmy sobie, że system bankowy nie jest najlepiej napisany i każdorazowe zasilenie konta kończy się przypisaniem do zmiennej salda jakiejś liczby (kwoty). Mamy teraz taką oto sytuację. Babcia postanawia zrobić wnuczkowi niespodziankę  i zasilić jego saldo kwotą 50zł. Uruchamia przeglądarkę i powoli realizuje operację. W końcu klika w przycisk celem wysłania przelewu i czeka… a że internet powolny, a  lokalna baza danych jest przeładowana operacjami to transakcja trwa. W tym samym czasie wnuczek po umyciu okien i otrzymaniu należnej opłaty postanawia także zasilić sobie konto (którego saldo to 100zł). Raz, dwa trzy i gotowe. 170 złotych na koncie. W tym czasie transakcja babci dochodzi do końca, a do salda wnuczka przypisana zostaje liczba 150. Co się stało? Tym ruchem babcia ogołociła wnuczka z pieniędzy, a raczej programiści, którzy nie słyszeli o lost update 😉

 

example_3

 

Nonrepeatable read występuje gdy kilka identycznych zapytań w ramach jednej transakcji daje nam różne wyniki. Spójrzmy na ten przykład. Użytkownik wyświetla dwa razy w ramach jednej transakcji stan swojego konta. W międzyczasie, ktoś zasila konto przez kiepsko zaimplementowany system bankowy. W rezultacie użytkownik każdorazowo otrzymuje różne wyniki. Może wydawać się to z jednej strony naturalne i poprawne. Skoro była zmiana to chcemy ją zobaczyć. Tu problem polega jednak na tym, że w trakcie trwania transakcji 1, transakcja 2 nie została wstrzymana i mogła swobodnie wykonać operację UPDATE. W przypadkach gdzie w ramach jednaj transakcji chcemy otrzymać zawsze te same dane może okazać się to problematyczne.

 

example_4

 

O phantom read mówimy gdy kilka, identycznych zapytań w jednej transakcji zwraca różną liczbę wierszy. Wyobraźmy sobie sytuację, w której administrator chce dwukrotnie wyświetlić listę użytkowników o Id mniejszym od 5. Dodajmy do tego fakt, że w bazie widnieje czterech takich userów o Id: 1,2,4,5. Po pierwszym zapytaniu zwrócone zostaną cztery wiersze. W tym czasie ktoś postanawia dodać użytkownika o Id równym 3. Podczas drugiego zapytania administratora zwrócone zostanie już pięć wierszy. Znów problem polega na tym, że podczas trwania jednej transakcji, druga może bezprawnie wykonać operację INSERT, która wpłynie na rezultat drugiego wywołania zapytania.

 

Po przykładach czas w końcu powiedzieć czym jest poziom izolacji 🙂 Definicja, którą sobie wymyśliłem brzmi tak:

 

Poziom izolacji definiuje możliwość wystąpienia wybranych anomalii w czasie trwania transakcji bazodanowej.

 

Innymi słowy, wybierając poziom izolacji dla naszej transakcji możemy z góry wykluczyć lub założyć wystąpienie pewnych anomalii 😛 W SQL Server poziomów izolacji jest kilka,  w EF jeszcze więcej, ale poniżej przedstawię te najpopularniejsze:

isolation_levels

 

Dodam, że domyślnym poziomem izolacji dla SQL Server jest Read Commited. W przypadku Entity Framework domyślnym poziomem jest poziom domyślny w providerze SQL.

 

Na dziś to wszystko. Mam nadzieję, że tematu za bardzo nie skomplikowałem i wszystko dało się zrozumieć 🙂 Już wkrótce wracamy do Daj się poznać z kolejnymi wpisami. Zachęcam również do śledzenia mojego twittera, żeby nie ominęły Was przyszłe wpisy 🙂 No to chyba wszystko. Lecę oglądać Hell’s Kitchen 😀

 

 


 

Odnośniki do literatury:

  • https://www.youtube.com/channel/UCCTVrRB5KpIiK6V2GGVsR1Q
  • https://msdn.microsoft.com/en-us/library/system.data.isolationlevel.aspx
  • https://msdn.microsoft.com/en-us/data/dn456843.aspx
  • http://www.c-sharpcorner.com/UploadFile/ff2f08/prevent-dead-lock-in-entity-framework/
  • http://dembol.org/blog/projektowanie/atrybuty-jakosciowe/spojnosc/

You may also like...