Transakcje Pojęcie transakcji Pojęcie transakcji stało się centralnym elementem w wielu współczesnych zastosowaniach baz danych. Jest kluczowym pojęciem pozwalającym zrozumieć zarówno kontrolę wielodostępu, jak i pojęcia odtwarzalności bazy danych. Transakcja jest to wykonywany program lub proces, na który składa się jeden lub więcej operacji dostępu do danych (takich jak np. odczytywanie danych, aktualizacja danych, itp.). Transakcja jest logiczną jednostką pracy w bazie danych. Transakcja może składać się z: pojedynczej operacji wykonywanej na bazie danych (np. polecenia wstawienia nowego rekordu danych poleceniem INSERT), dowolnej liczby operacji wykonywanych na bazie danych, całego programu aplikacji (dotyczącego działań wykonywanych w bazie) lub pewnej jego części. Uwaga. Z punktu widzenia bazy danych, wykonanie programu aplikacji można uważać za ciąg transakcji przeplatanych operacjami nie związanymi z bazą danych. Podstawowe operacje dostępu do danych należące do transakcji Baza danych jest zasadniczo reprezentowana jako zbiór nazwanych elementów danych. Rozmiar elementu danych określa się mianem ziarnistości (ang. granularity), a wśród typów elementów danych wyróżniamy: pole rekordu, rekord, blok dyskowy. Należy podkreślić, iż omawiane zagadnienia są niezależne od ziarnistości elementów danych. Przy pewnym uproszczeniu modelu bazy danych podstawowymi operacjami dostępu do danych, które mogą należeć do transakcji są: READ(X) odczytuje element bazy danych o nazwie X do zmiennej programowej, przy czym w celu uproszczenia zapisu przyjmujemy, że zmienna programowa ma tę samą nazwę co element, WRITE(X) zapisuje wartość zmiennej programowej X w elemencie bazy danych o nazwie X. Wykonanie polecenia READ(X) wiąże się z wykonaniem poniższych działań: 1) Znalezienie adresu bloku dyskowego, który zawiera element X. 2) Skopiowanie bloku dyskowego do bufora w pamięci głównej (o ile blok ten nie znajduje się już w którymś z buforów pamięci głównej). Skopiowanie elementu X z bufora do zmiennej programowej o nazwie X. Wykonanie polecenia WRITE(X) wiąże się z wykonaniem poniższych działań: 1) Znalezienie adresu bloku dyskowego, który zawiera element X. 2) Skopiowanie tego bloku dyskowego do bufora w pamięci głównej (o ile blok ten nie znajduje się już w którymś z buforów pamięci głównej). 3) Skopiowanie elementu X ze zmiennej programowej o nazwie X do odpowiedniej lokalizacji w buforze. 1
4) Przeniesienie zaktualizowanego bloku bufora z powrotem na dysk (od razu lub z pewnym opóźnieniem). Uwaga. Etap 4) jest tym, w którym dokonuje się faktyczna aktualizacja bazy danych na dysku. Przykłady transakcji Przykład 1. Przykładem prostej transakcji w rozważanym przykładzie sprzedaży produktów w hurtowni może być podwyżka ceny jednostkowej o 5% produktu o wskazanym numerze. W języku abstrakcyjnym możemy ją zapisać w postaci READ(nrproduktu = X, cenajednostkowa) nowa_cenajednostkowa = cenajednostkowa*1,05 WRITE(nrproduktu = X, nowa_cenajednostkowa) gdzie: READ(nrproduktu = X, cena jednostkowa) oznacza odczyt atrybutu cenajednostkowa krotki o wartości klucza głównego równej X. W rozważanym przypadku mamy do czynienia z transakcją składającą się z dwóch instrukcji dostępu do bazy danych (READ i WRITE), oraz instrukcji operacyjnej, czyli nie wykonywanej na bazie danych, tj. instrukcji nowa_cenajednostkowa = cenajednostkowa*1,05 Przykład 2. Przykładem nieco bardziej skomplikowanej transakcji w rozważanym przykładzie sprzedaży produktów w hurtowni może być usunięcie klienta o wskazanym numerze X. delete(kod_klienta = X) for all Zamówienia records, nrzam begin READ(nr_zamówienia = nrzam, kod_klienta) if(kod_klienta = X) then begin for all Opisy_zamówień records begin READ(nr_zamówienia = nrzam, nr_zamówienia) DELETE(nr_zamówienia) end DELETE(nr_zamówienia = nrzam) end end W rozważanym przypadku, oprócz usunięcia krotki relacji Klienci, musimy także odnaleźć wszystkie krotki relacji Zamówienia, które zawierają zamówienia złożone przez danego klienta, aby je usunąć z tej relacji. Ale, aby usunąć krotki relacji Zamówienia, musimy najpierw usunąć krotki relacji Opisy_zamówień, które opisują zamówienia złożone przez klienta przeznaczonego do usunięcia. 2
Własności transakcji Gdybyśmy nie wykonali powyższych modyfikacji, to wtedy baza danych utraciłaby integralność referencyjną i znalazłaby się w stanie niespójnym zawierałaby zamówienia, które nie byłyby przypisane żadnemu klientowi oraz zawierałaby opisy zamówień, których już nie byłoby w bazie. Transakcja powinna zawsze przeprowadzać bazę danych ze stanu spójnego w stan spójny, choć w trakcie jej trwania dopuszczalna jest możliwość naruszenia spójności. Przykładowo, rozważmy przypadek gdy któryś z pracowników zachoruje i wszystkie rozpoczęte przez niego zamówienia, a jeszcze niezakończone musi przejąć inny pracownik. Wówczas podczas wykonywania transakcji dotyczącej realizacji tej sytuacji może wystąpić moment, gdy jedna krotka relacji Zamówienia nową wartość kodu_pracownika, a druga nadal starą wartość kodu_pracownika. Jednak po zakończeniu transakcji, wszystkie krotki, których dotyczy modyfikacja w ramach tej transakcji, powinny mieć wpisaną nową wartość kodu_pracownika Rodzaje transakcji w oparciu o ich wynik Transakcja może zakończyć się na jeden z dwóch sposobów i związku z tym mówimy, że: transakcja została zatwierdzona, inaczej wypełniona (ang. commited) jeśli transakcja zakończy się sukcesem, transakcja została odrzucona (ang. aborted), inaczej wycofana (ang. rolled back) jeśli transakcja nie może być wykonana do końca z powodzeniem. Transakcji wypełnionej nie wolno wycofywać. W przypadku stwierdzenia, iż wykonanie transakcji było błędem, należy wykonać inną transakcję, tzw. transakcję kompensacyjną, która pozwoli odwrócić efekty błędnej transakcji. Transakcję wycofaną można powtórnie uruchamiać, i w zależności od powodu wycofania za pierwszym razem, za drugim uruchomieniem może udać się jej wykonanie do samego końca. Stany wykonania transakcji Diagram przejść stanów ilustrujący stany związane z wykonywaniem transakcji Transakcja przechodzi do stanu aktywnego (ang. active state) tuż po rozpoczęciu wykonania i wtedy mogą być wykonywane operacje ODCZYTAJ i ZAPISZ. W momencie kończenia transakcji przechodzi ona do stanu zatwierdzenia częściowego (ang. partially committed state). Po pomyślnym sprawdzeniu przez protokół zatwierdzania, o transakcji mówi się, iż osiągnęła swój punkt zatwierdzenia i przechodzi ona do stanu zatwierdzenia (ang. committed state). Po zatwierdzeniu transakcji jej wykonywanie jest zakończone i wszelkie wprowadzone zmiany muszą zostać trwale zapisane w bazie danych. Transakcja może także przejść do stanu awaryjnego (ang. failed state), jeżeli jedno ze sprawdzeń zakończy się niepowodzeniem lub transakcja zostanie anulowana w stanie aktywnym. Transakcja może wtedy zostać wycofana w celu anulowania efektów związanych z wykonanymi operacjami ZAPISZ na bazie danych. Stan zakończenia (ang. terminated state) odpowiada sytuacji, gdy transakcja opuszcza system. 3
SZBD a określenie transakcji SZBD samodzielnie nie posiada możliwości określenia, które operacje powinny częścią składową jednej logicznej transakcji. Z tego powodu to sam użytkownik musi być wyposażony w sposób określania granic transakcji. W języku SQL istnieją następujące instrukcje dotyczące działań na transakcjach: BEGIN TRANSACTION inicjowanie nowej transakcji, COMMIT kończenie transakcji przez zatwierdzenie wszystkich działań wykonanych podczas transakcji, ROLLBACK kończenie transakcji przez wycofanie wszystkich działań wykonanych podczas transakcji. Uwaga. Jeśli w programie nie występują powyższe instrukcje, to zazwyczaj traktuje się go jako pojedynczą transakcję i w takim przypadku automatycznie jest wykonywana przez system operacja zatwierdzania COMMIT, gdy kończy się ona powodzeniem, natomiast w przypadku przeciwnym, automatycznie jest wykonywana operacja wycofania ROLLBACK. Wielodostęp w bazach danych Zasadniczy sposób wykorzystania baz danych polega na umożliwieniu wielu użytkownikom jednoczesnego dostępu do baz danych. Jak wskazuje sama nazwa, wielodostępny system zarządzania bazą danych umożliwia wielu użytkownikom jednoczesny dostęp do bazy danych. Ta właściwość ma znaczenie, jeśli z pojedynczą bazą danych chcemy zintegrować wiele aplikacji. System zarządzania bazą danych musi zawierać wtedy oprogramowanie sterowania współbieżnego (ang. concurrency control). Jedną z jego podstawowych własności jest zapewnienie wielu użytkownikom w kontrolowany sposób możliwość podejmowania prób aktualizacji tych samych danych oraz zagwarantowanie, iż efekt tych działań nie spowoduje niespójności bazy danych. Przykładowo, jeśli wiele biur podróży spowoduje jednocześnie zarezerwować miejsce na wycieczce dla swoich klientów, system zarządzania bazą danych powinien zagwarantować dostępność każdego miejsca (np. w wycieczkowym autobusie) tylko dla jednego klienta obsługiwanego przez jedno z biur. Tego typu zastosowania są określane ogólnym mianem rozwiązań z przetwarzaniem transakcji na bieżąco ( ang. Online Transaction Processing OLTP). Własności transakcji SZBD jest zaopatrzony w pewien mechanizm, który wymusza wiele własności transakcji (właściwie to za zagwarantowanie poszczególnych własności transakcji są odpowiedzialne wyodrębnione składniki SZBD). Transakcja, którą można traktować jako zespół operacji wykonywanych na bazie danych (SELECT, INSERT, UPDATE, DELETE ), charakteryzuje się następującymi własnościami: niepodzielność (atomicity) spójność stanu (consistency) wyłączność (isolation) trwałość (durability) szeregowalność (serializability) własność niepodzielności (atomowość) (ang. atomicity) gwarantuje, iż albo wszystkie operacje przeprowadzane na danych przechowywanych w bazie danych w ramach pojedynczej transakcji zostaną 4
wykonane, albo żadna z nich nie zostanie przeprowadzona za zapewnienie tej własności odpowiada system odtwarzania SZBD. własność spójności (ang. consistency) gwarantuje, iż każda transakcja musi przekształcać bazę danych z jednego stanu spójnego w inny stan spójny zapewnienie tej własności leży zarówno po stronie SZBD, jak i po stronie użytkownika. własność izolacji (wyłączności) (ang. isolation) gwarantuje, iż każda transakcja zostanie wykonana w ten sposób, iż będzie odseparowana od pozostałych transakcji. własność trwałości (ang. durability) gwarantuje, iż rezultaty zakończonej z powodzeniem transakcji są na trwale zapisane w bazie danych i nie mogą być utracone w wyniku jakiegoś późniejszej awarii za zapewnienie tej własności odpowiada podsystem odtwarzania SZBD. własność szeregowalności (ang. serializability) efekt wykonania kilku transakcji jednocześnie (z przełączaniem zadań) musi być równoważny oddzielnemu wykonaniu każdej z nich w pewnej ustalonej kolejności. Wówczas transakcje takie nazywamy szeregowalnymi. Stan spójny bazy danych Stan spójny (consistent state) bazy danych spełnia więzy określone w jej schemacie, jak również wszelkie inne więzy, które powinny być zachowane. Można w pewnym uproszczeniu powiedzieć także, iż stan spójny bazy danych to stan, w którym wszelkie istniejące powiązania pomiędzy obiektami tworzą logiczną całość tzn. np. nie ma odwołań do obiektów nie istniejących w bazie. Z tego punktu widzenia program bazodanowy powinien być zatem napisany w sposób gwarantujący, iż jeżeli baza danych znajduje się w stanie spójnym przed wykonaniem transakcji, to będzie również w stanie spójnym po zakończeniu wykonywania transakcji, przy założeniu, iż nie występują żadne kolizje z innym transakcjami. Przyczyny powodujące załamanie procesu wykonania transakcji błędy systemu komputerowego (system crash) awarie związane z oprogramowaniem lub sprzętem, zaistniałe podczas wykonywania transakcji; prawie zawsze związane są z utratą zawartości pamięci operacyjnej komputera. błędy transakcji lub systemu zarządzania baza danych (SZBD) załamanie wykonania może być spowodowane np. próbą wykonania przez transakcję operacji dzielenia przez zero, przekroczeniem zakresu typu danych, niewłaściwą wartością parametru lub poleceniem BREAK wydanym przez operatora. błędy lokalne i wyjątki wykryte przez transakcje przykładem okoliczności zmuszających do przerwania transakcji jest np. w bankowej bazie danych, niewystarczający stan konta, uniemożliwiający wykonanie operacji przelewu pieniędzy. Transakcja sama powinna obsłużyć sytuację, wykonując operację ABORT. awarie dysku błędy zapisu lub odczytu danych z powierzchni dyskowych podczas wykonywania transakcji. kontrola współbieżności procesy bazy odpowiedzialne za kontrolę dostępu równoległego mogą zadecydować o konieczności przerwania transakcji i jej późniejszym restarcie; przyczyną może być niespełnienie warunku szereg owalności, czy też wykrycie stanu zakleszczenia. przyczyny zewnętrzne wynikające z zaniku napięcia, przypadkowego lub celowego zamazania danych przez operatora, pożaru, powodzi, kradzieży, itp. 5
Dziennik systemowy System przechowuje tzw. dziennik (ang. log) w celu umożliwienia odtwarzania stanu bazy danych po awariach mających wpływ na transakcje i przy jego pomocy śledzi wszystkie operacje związane z transakcjami, które mają wpływ na wartości elementów bazy danych. Dziennik jest przechowywany na dysku, a zatem nie mają na niego wpływu żadne awarie, oprócz dyskowych lub katastroficznych. Omówione teraz zostaną problemy, z jakimi mamy do czynienia w przypadku wykonywania transakcji w sposób niekontrolowany. Problem utraconej aktualizacji występuje w przypadku, gdy dwie transakcje uzyskujące dostęp do tych samych elementów bazy danych są związane z przeplotem ich operacji w taki sposób, iż wartości pewnych elementów bazy danych stają się błędne. Przykład. Rozpatrzmy przykład dwóch transakcji, dla których podczas ich wykonania mamy do czynienia z problemem utraconej aktualizacji. Problem aktualizacji tymczasowej występuje w przypadku, gdy jedna transakcja aktualizuje element bazy danych, a następnie transakcja ta z pewnego powodu kończy się niepowodzeniem. Jest on konsekwencją tego, iż do zaktualizowanego elementu dostęp uzyskuje inna transakcja, zanim zostanie on zmieniony na swoją oryginalną wartość. Przykład. Rozpatrzmy przykład dwóch transakcji, dla których podczas ich wykonania mamy do czynienia z problemem aktualizacji tymczasowej. Problem błędnej sumy występuje w przypadku, gdy jedna transakcja oblicza wartość funkcji agregującej podsumowania na wielu rekordach, podczas gdy inna transakcja aktualizuje niektóre z tych rekordów. W takim przypadku funkcja agregująca może uwzględnić w obliczeniach pewne wartości przed ich aktualizacją, a inne już po ich zaktualizowaniu. 6
Przykład. Rozpatrzmy przykład dwóch transakcji, dla których podczas ich wykonania mamy do czynienia z problemem błędnej sumy. Problem odczytu niepowtarzalnego (unrepeatable READ) występuje w przypadku, gdy jedna transakcja T 1 odczytuje element dwukrotnie, przy czym element ten zostaje zmieniony przez inną transakcję T 2 pomiędzy tymi dwoma odczytami transakcji T 1. Przykład. Rozpatrzmy przykład dwóch transakcji, dla których podczas ich wykonania mamy do czynienia z problemem odczytu niepowtarzalnego. Harmonogram Kolejność wykonywania operacji związanych (blokowanie, odczyt, zapis, itp.) z różnymi transakcjami w przypadku, gdy transakcje są wykonywane współbieżnie w technice przeplotu określa się mianem harmonogramu (schedule) zbioru transakcji (w skrócie, hamonogramem) albo historii. Harmonogram S, inaczej historia, zbioru n transakcji T 1, T 2,, T n jest uporządkowaniem operacji transakcji podlegającym ograniczeniu, które określa, że dla każdej transakcji T i należącej do harmonogramu S, operacje tej transakcji w S muszą występować w tej samej kolejności, w jakiej występują w T i. Należy jednak podkreślić fakt, iż operacje związane z innymi transakcjami T j mogą się przeplatać z operacjami transakcji T i. Lp. T 1 T 2 1. READ(X) 2. READ(X) 3. X = X 100 4. WRITE(X) 5. X = X + 100 6. WRITE(X) 7
Operacje konfliktowe Mówimy, iż dwie operacje w harmonogramie są w stanie konfliktu, gdy spełniają następujące trzy warunki: należą do różnych transakcji, uzyskują dostęp do tego samego elementu X, przynajmniej jedna z operacji jest operacją WRITE(X). Przykład. Rozpatrzmy harmonogram w skróconej notacji opisu rozpatrzony na slajdzie dotyczącym przedstawienia problemu utraconej aktualizacji: S: READ 1 (X); READ 2 (X); WRITE 1 (X); READ 1 (Y); WRITE 2 (X); WRITE 1 (Y); Przykładowo, w harmonogramie S operacje READ 1 (X) oraz WRITE 2 (X) są w konflikcie, podobnie jak operacje READ 2 (X) i WRITE 1 (X), a także operacje WRITE 1 (X) i WRITE 2 (X). Natomiast operacje READ 1 (X) i READ 2 (X) nie są w konflikcie, ponieważ obie są operacjami odczytu. Także i operacje WRITE 2 (X) i WRITE 1 (Y) nie są w konflikcie, gdyż operują na odrębnych elementach bazy danych. Z kolei operacje READ 1 (X) i WRITE 1 (X) nie są w konflikcie, gdyż stanowią część składowej tej samej transakcji. Harmonogram sekwencyjny i szeregowalny Definicja. Harmonogram zbioru transakcji nazywamy sekwencyjnym (serial), jeśli wszystkie operacje każdej transakcji występują kolejno po sobie. Lp. T 1 T 2 1. READ(X) 2. X = X 100 3. WRITE(X) 4. READ(X) 5. X = X + 100 6. WRITE(X) Jak wynika z definicji harmonogramu sekwencyjnego, w danym momencie tylko jedna transakcja jest aktywna zatwierdzenie (lub anulowanie) transakcji aktywnej inicjuje wykonywanie kolejnej. Definicja. Harmonogram transakcji nazywamy szeregowalnym (seriazable), jeśli jego wynik jest równoważny wynikowi otrzymanemu za pomocą pewnego harmonogramu sekwencyjnego. Przykład ilustrujący różne typy harmonogramów Przykład. W poniższym przykładzie zilustrujemy różnice pomiędzy różnymi typami harmonogramów. W tym celu rozważmy dwie transakcje przelewu pewnych kwot pieniężnych z konta na konto. T 1 : READ(A), A = A - 100, WRITE(A); READ(B), B = B + 100, WRITE(B); T 2 : READ(B), B = B - 200, WRITE(B); READ(C), C = C + 200, WRITE(C); każdy harmonogram sekwencyjny powyższych transakcji ma własność zachowania sumy, tj. A + B + C; przestawienia kolejności wykonywania pojedynczych operacji wewnątrz transakcji mogą doprowadzić do stworzenia następujących harmonogramów: niesekwencyjnych, ale szeregowalnych (sytuacja pożądana); albo nieszeregowalnych (sytuacja niepożądana). 8
Przykład harmonogramu sekwencyjnego Przykład harmonogramu szeregowalnego, niesekwencyjnego Przykład harmonogramu nieszeregowalnego Teoria szeregowalności Ponieważ zawsze jest możliwe, aby transakcje były wykonywane po kolei (sekwencyjnie), jest sensownie zakładać, iż normalny lub zamierzony wynik jest taki sam jak wynik otrzymany wtedy, kiedy nie jest wykonywana współbieżnie żadna inna transakcja. Przyjmuje się zatem, iż współbieżne wykonanie kilku transakcji jest poprawne wtedy i tylko wtedy, gdy jego wynik jest taki sam jak wynik otrzymany przy 9
sekwencyjnym wykonaniu tych samych transakcji w pewnej kolejności. Załóżmy zatem, iż współbieżne wykonanie pewnej liczby transakcji jest poprawne wtedy i tylko wtedy, gdy jego wynik jest taki sam jak wynik otrzymany przy sekwencyjnym wykonaniu tych samych transakcji w pewnej kolejności. Techniki sterowania współbieżnego Istnieją dwie podstawowe techniki sterowania współbieżnego, które mogą być wykorzystane w celu zapewnienia własności szeregowalności oraz niekolidowania, czyli izolacji, transakcji wykonywanych współbieżnie. pierwsza grupa tych metod dotyczy zastosowania tzw. programów szeregujących, które są częścią systemu zarządzania bazą danych odpowiedzialną za rozstrzyganie konfliktów pomiędzy sprzecznymi żądaniami. Pogramy te mogą eliminować sytuacje zakleszczenia i nieszeregowalności poprzez przerwanie wykonania i restart w późniejszym terminie jednej bądź większej ilości transakcji. Dotychczasowe efekty działania przerwanych transakcji są wtedy wymazywane z bazy danych. druga grupa tego typu technik zapewnia szeregowalność harmonogramów przy użyciu protokołów (tzn. zestawu reguł i zasad) gwarantujących szeregowalność, które muszą być stosowane przez wszystkie transakcje. Zależnie od protokołu zasady te mają różne przeznaczenie: zapewniają szeregowalność transakcji, eliminują powstawania problemów powstających przy stosowaniu blokad (zakleszczenie, zagłodzenie). Definicja blokady Jeden z ważniejszych zbiorów protokołów stosuje technikę blokowania (locking) elementów danych w celu zapobieżenia współbieżnemu uzyskiwaniu dostępu do elementów przez wiele transakcji. Blokada (lock) jest zmienną związaną z elementem danych, która opisuje stan tego elementu pod względem możliwości działań, jakie mogą być na nim wykonane. Ogólnie rzecz ujmując, na każdy pojedynczy element bazy danych przypada jedna blokada. Blokowanie binarne Najprostszym rodzajem kontroli współbieżności jest tzw. blokowanie binarne. Polega ono na skojarzeniu z każdym obiektem bazy znacznika wskazującego na zezwolenie na dostęp do obiektu lub jego brak. Blokada binarna (binary block) może posiadać dwa stany lub wartości: zablokowany (lock) i odblokowany (unlock) (dla uproszczenia 1 i 0). Z każdym elementem X bazy danych jest związana odrębna blokada. Jeśli wartość blokady dla elementu X wynosi 0, to operacje bazodanowe mogą uzyskać dostęp do tego elementu, zaś w przypadku, gdy wartością blokady jest 1, dostęp do elementu nie jest możliwy. Znacznik taki może być rekordem postaci <id obiektu bazy, stan blokady>. Rekordy te mogą być na przykład przechowywane w pewnych tabelach np. gdy blokowanie jest na poziomie wiersza może to być tabela: ROWID AAAQ2UAAEAAABYGAAA Stan_blokady LOCK.... Blokowanie binarne Każda transakcja, która usiłuje wykorzystać wartość elementu X, musi zawierać w swojej treści polecenia wykonania operacji LOCK(X) i UNLOCK(X). O tym, czy transakcja otrzyma prawo dostępu do obiektu, decyduje proces systemu zarządzania bazą danych nazywany menadżerem blokad (lock manager), który działa według następującego algorytmu: 10
gdy transakcja żąda dostępu do obiektu X. jeśli LOCK(X) == 0 -- tzn. element X nie jest zablokowany przez inną transakcję wtedy LOCK(X) 1 -- zablokowanie elementu w przeciwnym przypadku czekaj aż LOCK(X) = 0 -- obiekt zostanie odblokowany i menadżer blokad uruchomi transakcję); -- transakcja jest ustawiana w kolejce transakcji oczekujących na pożądany zasób (dane z bazy) gdy transakcja zwalnia blokadę obiektu X. LOCK(X) 0 -- odblokowanie elementu X jeśli jakieś inne transakcje oczekują na dostęp do X, wtedy uruchom którąś z oczekujących transakcji i wznów jej działanie Reguły obowiązujące transakcję w blokowaniu binarnym Aby zapewnić poprawność procesu blokowania binarnego, każda transakcja T musi przestrzegać poniższych zasad: 1. Transakcja T musi wykonać operację LOCK(X) przed jakąkolwiek z operacji READ(X) lub WRITE(X). 2. Transakcja T musi wykonać operację UNLOCK(X) po zakończeniu wszystkich operacji READ(X) i WRITE(X). 3. Transakcja T nie może wykonać operacji LOCK(X), jeśli już posiada blokadę elementu X. 4. Transakcja T nie może wykonać operacji UNLOCK(X), o ile nie wykonała wcześniej operacji LOCK(X). Zadanie stosowania reguł 1, 3, 4 przejmuje na siebie poprawnie działający menadżer transakcji, zaś reguła 2 musi być przestrzegana przez programistę systemu bazodanowego. Główną wadą blokowania binarnego jest jego zbyt wielka restrykcyjność, ponieważ tylko jedna transakcja może trzymać blokadę na danym elemencie. Stąd wynika, iż żadne dwie transakcje nie mogą uzyskać współbieżnego dostępu do tego samego elementu. Mechanizm blokad - blokowanie zapisu i blokowanie aktualizacji Ulepszeniem metody blokowania binarnego jest mechanizm oddzielnego blokowania zapisu i odczytu. W metodzie tej można wyróżnić dwa rodzaje blokad: 1. Blokada do zapisu (READ_LOCK) żadna transakcja nie może aktualizować obiektu, który został zamknięty tą blokadą przez jakąkolwiek inną transakcję. Natomiast możliwa jest sytuacja, że wiele transakcji czyta dane z tego samego obiektu (dlatego też ten rodzaj nazywany jest trybem dzielonego dostępu do czytania - READ-sharable) 2. Blokada do aktualizacji (WRITE_LOCK blokada całkowita) żadna inna transakcja, z wyjątkiem tej, która nałożyła blokadę, nie ma dostępu (zarówno do czytania, jak i aktualizacji) do danego obiektu (tryb wyłącznego dostępu do pisania - WRITE-exclusive). Mechanizm blokad - blokowanie zapisu i blokowanie aktualizacji Zależności pomiędzy poszczególnymi trybami blokowania przez transakcje T 1 i T 2 można przedstawić w tabeli: T1 T2 READ_LOCK(X) WRITE_LOCK(X) READ_LOCK(X) dozwolony niedozwolony WRITE_LOCK(X) niedozowolony niedozwolony 11
Jak widać, blokada zapisu (READ_LOCK) przez jedną transakcję nie wyklucza możliwości zastosowania takiej samej blokady na tym samym obiekcie przez inną transakcję. Realizację programową opisanego sposobu blokowania, możemy zapewnić poprzez wykorzystanie rekordów postaci: <id obiektu, stan blokady, ilość blokad READ_LOCK >. Pole stan blokady może przyjmować trzy różne wartości (odpowiednio zakodowane): zablokowane_do_odczytu (WRITE_LOCK), zablokowane_do_aktualizacji (READ_LOCK), niezablokowane. Obie blokady: zapisu i całkowitą usuwa operacja UNLOCK. Mechanizm blokad - blokowanie zapisu i blokowanie aktualizacji Analogicznie jak dla blokowania binarnego, transakcja musi przestrzegać pewnych reguł: wykonuje operacje WRITE_LOCK lub READ_LOCK przed jakąkolwiek operacją READ; wykonuje operację WRITE_LOCK przed jakąkolwiek operacją WRITE; nie próbuje odblokować jednostki, której w żaden sposób nie blokuje; nie próbuje całkowicie zablokować jednostki (WRITE_LOCK), którą już w ten sposób blokuje; nie próbuje zablokować do zapisu jednostki (READ_LOCK), którą blokuje w jakikolwiek sposób. Protokół dwufazowy i sprawdzanie szeregowalności Algorytm sprawdzania szeregowalności harmonogramu S z blokadami: zapisu (READ_LOCK) i całkowitymi (WRITE_LOCK), opiera się na konstrukcji grafu zorientowanego G, w którym wierzchołki odpowiadają transakcjom, a krawędzie wyznacza się korzystając z następujących reguł: 1. jeśli transakcja T i z harmonogramu blokuje zapis elementu A {READ_LOCK(A)}, a T j jest następną transakcją (o ile taka istnieje), która chce całkowicie zablokować A { WRITE LOCK(A)}, to prowadzimy krawędź T i T j. 2. Jeśli transakcja T i z harmonogramu blokuje całkowicie {WRITE_LOCK(A)}, a T j jest następną transakcją (o ile taka istnieje), która chce całkowicie zablokować A {WRITE_LOCK(A)}, to prowadzimy krawędź T i T j. 3. Jeśli T m jest transakcją blokującą zapis A {READ LOCK(A)}, po tym, gdy T i zdjęła swoją całkowitą blokadę {UNLOCK(A)}, lecz zanim T j całkowicie zablokowała A {WRITE_LOCK(A)}, (jeśli T j nie 12
istnieje, to T m jest dowolną transakcję blokującą zapis A po odblokowaniu jej przez T i ), to prowadzimy krawędź T i T m. Szeregowalność danego harmonogramu rozstrzygamy, sprawdzając istnienie cykli w tak skonstruowanym grafie (nazywanym również grafem pierwszeństwa). Jeśli G zawiera cykl, to harmonogram S nie jest szeregowalny. Twierdzenie. Przedstawiony algorytm sprawdzania szeregowalności w poprawny sposób stwierdza szeregowalność harmonogramu. Dowód poprawności powyższego algorytmu znajduje się m.in. w książce J.D.Ullmana Systemy baz danych. Graf pierwszeństwa dla rozważanego harmonogramu jest postaci: Ponieważ graf pierwszeństwa zawiera cykle, więc rozważany harmonogram jest nieszeregowalny. W przypadku wykrycia cyklu, należy uruchomić mechanizmy cofające efekt działania transakcji T powodującej konflikt, zawiadomić o fakcie program aplikacyjny lub zrestartować T w późniejszym terminie. 13
Sam mechanizm blokad nie zapewnia szeregowalności dowolnego harmonogramu wykonania transakcji. Stąd też nie gwarantuje utrzymania bazy danych w stanie spójnym. Gwarancję taką daje zastosowanie protokołu dwufazowego, którego jedynym wymaganiem jest, aby transakcje wykonały blokowania wszystkich wymaganych przez siebie obiektów przed odblokowaniem któregokolwiek z nich. Transakcje, które przestrzegają tego protokołu uważane są za dwufazowe w takim sensie, że operacje zablokowania i odblokowania wykonywane są w dwóch różnych fazach w trakcie działania transakcji. Jeżeli wszystkie transakcje są dwufazowe, ich równoległe wykonanie zawsze będzie szeregowalne. Prawdziwe jest następujące twierdzenie: Twierdzenie. Dowolny harmonogram S transakcji dwufazowych jest harmonogramem szeregowalnym. Zaletą protokołu dwufazowego jest fakt, iż przy niewielkich wymaganiach i stosunkowo niewielkim nakładzie pracy w implementacji, gwarantuje on szeregowalność harmonogramów transakcji. Wadą natomiast jest to, że blokowanie dwufazowe może ograniczyć współbieżność w bazie danych. Często zdarza się, że transakcja T nie może zwolnić blokady na obiekcie X po jego wykorzystaniu, ponieważ w dalszym planie swego działania przewiduje blokadę na innym obiekcie Y. Aby zwolnić obiekt X, transakcja musi zablokować obiekt Y wcześniej niż jest to wymagane do jej działania. Zablokowanie Y wówczas, gdy staje się on niezbędny, pociąga za sobą konieczność zbędnego przetrzymania blokady X. W obu przypadkach obiekt X musi pozostać zablokowany, aż do chwili gdy T uzyska wszystkie wymagane blokowania. W tym czasie inne transakcje wymagające dostępu do X są postawione w stan oczekiwania, pomimo iż transakcja T nie korzysta już z X. Jeżeli natomiast Y zostaje zablokowane przed czasem, działania transakcji wymagających dostępu do Y jest wstrzymywane, pomimo że T nie korzysta jeszcze z obiektu Y. Protokół blokowania dwufazowego zapewnia szeregowalność transakcji, ale nie eliminuje możliwości wystąpienia sytuacji: zakleszczenia (deadlock) impasu (livelock), wystąpujących we wszystkich systemach sterujących współbieżnością w oparciu o mechanizmy blokowania. W dalszej części rozważań zostaną przedstawione przyczyny powstawania problemu i techniki jego eliminacji. Zagłodzenie (impas, uwięzienie) Zagłodzenie (starvation) ma miejsce podczas blokowania, gdy transakcja nie może kontynuować działania przez nieokreślony czas, podczas gdy inne transakcje w systemie są realizowane bez przeszkód w sposób normalny. Sytuacja taka może zaistnieć, gdy mechanizm przyznawania blokad (lock manager) nie jest skonstruowany poprawnie i daje pierwszeństwo pewnym typom transakcji. Niestety, praktycznie niemożliwe jest wykrycie impasu w trakcie wykonywania danego programu realizującego transakcje. Łatwo co prawda zaobserwować, że pewna transakcja czeka bardzo długo na zdarzenie (np. możliwość zablokowania zasobu), które wystąpiło już wiele razy, ale nie wiadomo, jak system zachowa sie w przyszłości. Aby uniknąć problemu, należy zastosować maksymalnie optymalny menadżer blokad. Jednym z takich rozwiązań jest zastosowanie techniki kolejek FIFO: pierwszy wszedł pierwszy będzie obsłużony. Obsługuje ona transakcje w kolejności zgłoszenia potrzeby zablokowania obiektu. 14
Innym rozwiązaniem jest dopuszczenie różnych priorytetów dla różnych transakcji, ale równocześnie wprowadzenie procesu podnoszenia priorytetu transakcjom długo oczekującym na blokadę. Transakcje te mogą w końcowym stadium osiągnąć najwyższy priorytet i dzięki temu mają zapewnioną realizację. Zakleszczenie Zakleszczenie (deadlock) występuje wtedy, gdy każda transakcja T ze zbioru dwóch lub więcej transakcji oczekuje na pewien element zablokowany przez inną transakcję T z tego zbioru. To powoduje, iż każda transakcja ze zbioru znajduje się w kolejce oczekującej i czeka na inną transakcję ze zbioru na zwolnienie blokady. Możliwość powstania tego problemu tzn. zakleszczenia najlepiej ilustruje poniższa sekwencja działań: 1. Transakcja T 1 blokuje całkowicie jednostkę A, 2. Transakcja T 2 blokuje całkowicie jednostkę B, 3. T 1 chce nałożyć całkowitą blokadę na jednostkę B, ale musi czekać, bo jest ona zablokowana przez T 2, 4. T 2 chce nałożyć całkowitą blokadę na jednostkę A, ale musi czekać, bo jest ona zablokowana przez T 1. T 1 T 2 1. WRITE_LOCK(A) 2. WRITE_LOCK(B) 3. WRITE_LOCK(B) 4. WRITE_LOCK(A) 5.. Jak widać, w rozważanej sytuacji ani transakcja T 1, ani T 2 nie mogą być kontynuowane - występuje zakleszczenie. Zakleszczenie metody zapobiegania Można spotkać różne metody zapobiegające powstawaniu zakleszczeń. Przykładem takiej metody jest strategia wstępnego rezerwowania obiektów (pre-claim strategy). Każda transakcja przed rozpoczęciem działania nakłada blokady na wszystkie obiekty, z których będzie korzystać w trakcie swej realizacji. Taka strategia charakteryzuje się jednak pewnymi wadami: może powodować, że przy dużym stopniu jednoczesności dostępu (rozumianym jako duża liczba transakcji rywalizujących o zasoby) wiele transakcji będzie musiało długo czekać na rozpoczęcie swego działania. przed rozpoczęciem realizacji nie zawsze znany jest pełen zbiór obiektów, do których będzie potrzebny dostęp. Taki zbiór może być wyznaczany dynamicznie podczas realizacji transakcji i generalnie może zależeć od danych wejściowych, stanu bazy i algorytmu transakcji. Strategia taka jest więc mało praktyczna w systemach bazodanowych. Właściwszą metodą wydaje się dopuszczenie możliwości powstawania zakleszczeń, ale równocześnie stworzenie mechanizmów do ich wykrywania i eliminowania. W wykrywaniu zakleszczenia może być pomocne generowanie grafu bieżących transakcji według następującej reguły: 15
jeśli transakcja T 1 żąda blokady jednostki, która jest zablokowana przez transakcję T 2, wówczas węzły T 1 i T 2 łączymy krawędzią. Przykład. T 1 T 2 1. WRITE_LOCK(A) 2. 3. WRITE_LOCK(B) 4. 5. WRITE_LOCK(B) 6. WRITE_LOCK(A) Generujemy graf bieżących transakcji dla rozważanych transakcji dodanie tego rekordu utworzy cykl i tym samym wykaże zakleszczenie Graf bieżących transakcji dla powyższych transakcji: 16