Tematyką wytwarzaniem oprogramowaniem zajmuje się już ponad dziesięć lat i z moich obserwacji wynika, że są pewne tematy o których programiści/projektanci/architekci wiedzą, że są jednak ich wiedza na ten temat jest mocno wyrywkowa albo po prostu oparta na pewnych mitach. Pierwszy taki temat o którym chciałbym napisać to używanie transakcji w systemach informatycznych, tak się składa że ostatnio też musiałem powrócić do tej tematyki w związku ze zmianami architektury naszego systemu.
Może na początek podam definicje transakcji, cytując za Wikipedią:
Transakcja - zbiór operacji na bazie danych, które stanowią w istocie pewną całość i jako takie powinny być wykonane wszystkie lub żadna z nich. Warunki jakie powinny spełniać transakcje bardziej szczegółowo opisują zasady ACID (Atomicity, Consistency, Isolation, Durability - Atomowość, Spójność, Izolacja, Trwałość).
Żeby operacja była transakcyjną musi spełniać zasady ACID i może warto ten skrót bardziej rozwinąć:
- Atomowość transakcji - właściwość gwarantująca niepodzielność kroków w wykonywanej operacji. Czyli albo wszystkie kroki operacji zostają wykonane albo żaden z nich. Przykładem ilustrującym tą właściwość mogą być dwa kroki realizowane w trakcie transferu środków w systemie bankowym między dwoma kontami. Przy takiej operacji najpierw odejmujemy kwotę z jednego konta a później dodajemy tą kwotę do drugiego konta. Atomowość gwarantuje, że wykonana zostaną dwa kroki albo żaden z nich.
- Spójność transakcji - gwarantuje, że po operacji system będzie spójny, inaczej mówiąc nie zostaną naruszone zasady integralności systemu. Najprostszym przykładem może być wstawienie wiersza w tabeli z obywatelami Polski gdzie kluczem unikalnym (czyli gwarantującym unikalność) jest zasada mówiąca, że każdy obywatel Polski ma unikalny PESEL (co w praktyce nie jest do końca prawdą bo zdarzały się przypadki, że ten numer nie jest unikalny co sugeruje, że w systemie pojawił się problem z transakcjami:) ).
- Izolacyjność transakcji - gwarantuje, że dwie współbieżne operacje nie widzą efektów swojego działania do czasu zatwierdzenia transakcji (czyli operacji COMMIT). Ze względu na to, że gwarantując całkowitą izolacyjność transakcji tracimy na możliwości wykonywania równoległych operacji na danych w większości systemów możemy ustawiać tzw. poziom izolacji. O poziomie izolacji napiszę w dalszej część artykułu bo jest to jeden z tematów, który jest słabo znany przez programistów.
- Trwałość - oznacza, że system potrafi udostępnić spójne i nienaruszone dane zapisane w ramach zatwierdzonych transakcji po awarii spowodowanej np. zanikiem napięcia.
Przy omawianiu ACID wspomniałem o czymś takim jak poziom izolacji, z moich obserwacji programiści często błędnie zakładają, że jak już wykonują operacje w transakcji nic złego nie może się zdarzyć. Niestety zwykle poziom izolacji jest tak ustawiony, że przy niektórych scenariuszach mogą się pojawić ‘ciekawe przypadki’. Wymagana jest świadomość jakie ‘złe rzeczy’ mogą się wydarzyć na określonych poziomach. Dlatego zacznę od listy ‘złych rzeczy’, które mogą się pojawić przy operacjach objętych transakcją:
- dirty read - transakcja może przeczytać dane zapisane przez inną niezakończoną transakcje
- nonrepeatable read - w transakcji ponownie odczytujemy dane, które wcześniej były odczytane w tej samej transakcji i okazuje się, że dane uległy modyfikacji w wyniku innej zakończonej transakcji (czyli były zmodyfikowane między kolejnymi odczytami)
- phantom read - ponowne wykonanie zapytanie z tymi samymi warunkami wyszukiwania zwraca inny zbiór wierszy - inna zakończona transakcja dodała nowe wiersze.
Korzystając z określonego poziomu izolacji unikamy albo jesteśmy narażeni na wymienione problemy. Tabelka poniżej prezentuje na co jesteśmy narażeni w przypadku określonego poziomu izolacji:
| Poziom izolacji |
Dirty Read |
Nonrepeatable Read |
Phantom Read |
| Read uncommitted |
Możliwe |
Możliwe |
Możliwe |
| Read committed |
Niemożliwe |
Możliwe |
Możliwe |
| Repeatable read |
Niemożliwe |
Niemożliwe |
Możliwe |
| Serializable |
Niemożliwe |
Niemożliwe |
Niemożliwe |
Należy zaznaczyć, że nie wszystkie systemy wspierają wszystkie poziomy izolacji i trzeba mieć tego świadomość w trakcie pisania oprogramowania. Przykładowo Postgres wspiera tylko dwa poziomy - serializable i read committed chociaż teoretycznie dostępne są wszystkie.
Mogłoby się wydawać po dotychczasowej lekturze, że najlepiej dla tworzącego oprogramowania byłoby używać poziomu serializable, wtedy unikniemy wszystkich problemów i to jest ‘prawie’ prawdą. Prawie ponieważ problemów z transakcyjnością unikniemy ale niechybnie pojawią się problemy z wydajnością. Przy poziomie serializable wszystkie operacje wykonywane są szeregowo, w epoce gdzie producenci serwerów prześcigają się w ilości rdzeni takie ograniczenie jest nie do zaakceptowania bo większość operacji na serwerach z założenia wykonywanych jest równolegle.
Jeśli już uporządkowaliśmy wiedzę odnośnie transakcji na koniec warto wspomnieć jakie są możliwości zarządzania transakcjami w programach. Wyróżnia się trzy modele zarządzania transakcjami:
Local Transaction Model – w tym przypadku programista zarządza połączeniami do źródeł danych np. baz danych przez JDBC i wywołuje operacje zatwierdzenia (COMMIT) albo wycofania (ROLLBACK) transakcji na konkretnym połączeniu, właściwie programista zarządza połączeniami niż transakcjami. Model ten jest dobrze znany programistą którzy używają JDBC w Javie gdzie otwieramy połączenie do bazy i na tym połączeniu wywołujemy metody commit() i rollback(). Poniżej przykład stosowania transakcji przy operacjach na bazie z użyciem JDBC
……….
Connection conn = ds.getConnection();
conn.setAuoCommit(false);
try {
OPERACJA NA BAZIE
con.commit();
} catch (Exception e) {
conn.rollback();
throw e;
} finally {
stmt.close();
conn.close();
}
Programmatic Transaction Model – w tym modelu programista zarządza transakcjami a nie połączeniami. Przykładem takiego rozwiązania jest interfejs JTA w Javie. Pozwala on na wywołanie kilkunastu operacji na różnych źródłach danych w obrębie jednej transakcji. W związku z tym, że JTA to tylko interfejs w celu użycia mechanizmów zarządzania transakcjami trzeba użyć jakieś dostępnej implementacji, w przypadku Javy możemy użyć niezależnego serwera transakcji typu Atomikos albo BTM. Poniżej przykład zarządzania transakcjami z użyciem JTA, zwróćcie uwagę, że commit() wykonywany jest na obiekcie utm a nie na conn (połączeniu do bazy).
UserTransactionManager
utm=new UserTransactionManager();
utm.init();
utm.begin();
Connection conn = ds.getConnection();
OPERACJA NA BAZIE
conn.close();
utm.commit();
Declarative Transaction Model – w przypadku EJB nazywany Container Managed Transaction. W tym modelu kontener w którym jest osadzony program zarządza transakcjami, programista poprzez opis w XML albo korzystając z adnotacji opisuje, które operacje powinny rozpoczynać albo dołączać do istniejącej transakcji, może również zdecydować jaki stopień izolacji ma być ustawiony przy każdej operacji. Ten model używany jest w EJB w serwerach aplikacji zgodnych z standardem J2EE albo w frameworku Spring.
To tyle w pierwszym artykule z cyklu ‘Tematy proste ale..’, mam nadziej, że chociaż trochę wyjaśniłem tematykę użycia transakcji.