Rozdział 23
Sterowanie współbieżnością
Termin sterowanie współbieżnością (ang. concurrency control) odnosi się do
jednoczesnego współdzielenia zasobów przez wielu użytkowników. Można tu
wyróżnić dwa główne zagadnienia: izolowanie transakcji i systemy sterowania
współbieżnością. Poziomy izolowania transakcji określają zakres dostępu
w ramach danej transakcji dla modyfikacji bazy danych przeprowadzanych
w innych współbieżnych transakcjach. Systemy sterowania współbieżnością ściśle
wiążą się z zagadnieniem izolowania transakcji - kontrolują one nie tylko zakres
dostępu w ramach jednej transakcji - dla aktualizacji przeprowadzanych przez inne
transakcje, ale również sposób przeprowadzania tych aktualizacji.
Izolowanie transakcji
Terminem Izolowanie transakcji (ang. transaction isolation) opisujemy
mechanizm wykorzystywany przez systemy zarządzania bazami danych dla
odseparowania danej transakcji od rezultatów działania innej. Izolowanie
transakcji w Delphi odbywa się na trzech oddzielnych poziomach, z których każdy
ma odrębny charakter. Poziomy te (w skrócie poziomy TIL, od słów Transaction
Isolation Level) określają zakres dostępu, jakim możemy dysponować w ramach
danej transakcji do modyfikacji dokonywanych w bazie danych przez inną
transakcję współbieżną.
System Delphi obsługuje za nas problemy związane z transakcjami, tzn. sam ustala
domyślny poziom TIL oraz automatycznie rozpoczyna i zatwierdza transakcje
podczas uaktualniania bazy danych przez naszą aplikację. Szerszą kontrolę
zapewni nam Passthrough SQL lub komponent
TDatabase
. Transakcjami zaleca
się sterować za pośrednictwem komponentu
TDatabase
, gdyż tylko wówczas,
przetwarzane przez nas transakcje będą widoczne dla Delphi.
Wybór odpowiedniego poziomu izolowania transakcji
Większość serwerów baz danych, łącznie z InterBase, obsługuje trzy oddzielne
poziomy izolowania transakcji. Ujmując rzecz ściśle z
punktu widzenia
zarządzania, w całej aplikacji powinno się stosować ten sam poziom TIL. Jeszcze
lepszym podejściem jest przyjęcie tego samego poziomu TIL we wszystkich
aplikacjach bazy danych. Taka strategia projektowa zapewni lepszą ochronę
650
Część IV
integralności bazy danych i sprawi, że efekty przeprowadzanych modyfikacji będą
zgodne z naszymi oczekiwaniami.
Z drugiej strony jednak, wymagania konkretnej aplikacji mogą uniemożliwić nam
przyjęcie tak jednolitej strategii. Obstawanie przy niej za wszelką cenę może
prowadzić do zbyt restrykcyjnej lub niepewnej ochrony integralności bazy danych.
Mogłoby wówczas okazać się, że główne czynności naszej bazy to niepotrzebne
blokowanie (lock) tablic lub zapobieganie nieszkodliwym aktualizacjom.
WSKAZÓWKA
Implementując poziomy TIL musimy najpierw zapewnić integralność bazy danych,
a dopiero potem optymalizować wydajność na drodze udoskonalania przyjętego
schematu izolowania transakcji.
Klasyczne problemy izolowania transakcji
Problemy występujące przy wyborze schematów izolowania transakcji można
podzielić na pięć grup:
odczyty pozorne (ang. dirty reads): mogą wystąpić, gdy w ramach danej
transakcji odczytuje się nie zatwierdzone modyfikacje wprowadzone w innej
transakcji. Ponieważ nie zatwierdzoną modyfikację można wycofać (ang. roll
back), rezultatem jej odczytania w danej transakcji (jeszcze zanim została
wycofana), będzie właśnie odczyt pozorny.
odczyty niepowtarzalne (ang. nonrepeatable reads): występują wtedy, gdy
danej transakcji zezwala się na modyfikację wierszy, do których odczytu ma
dostęp inna transakcja. Odczyty takie nazywamy niepowtarzalnymi, ponieważ
kolejne odczyty - z powodu modyfikacji wprowadzanych w danej transakcji ze
strony innej transakcji - będą się między sobą różnić. Ze względu na swoją
naturę, transakcja poziomu
READ COMMITTED
umożliwia odczyty
niepowtarzalne, ponieważ modyfikacje dokonane w ramach innych transakcji
mogą być odczytywane w trakcie zatwierdzania.
wiersze-widma (ang. phantom rows): pojawiają się wtedy, gdy w transakcji
zezwala się na wybór niekompletnego zbioru nowych wierszy zapisanych
w ramach innej transakcji. Poziom izolowania transakcji, określany jako
READ
COMMITTED,
nie zapobiega występowaniu wierszy-widm.
zgubione aktualizacje (ang. lost updates): występują wtedy, gdy w ramach
transakcji omyłkowo nadpisana zostanie (overwrite) modyfikacja dopiero co
wprowadzona w innej współbieżnej transakcji.
Rozdział 23 Sterowanie współbieżnością
651
efekty uboczne aktualizacji (ang. udpdate side effects): występują wówczas, gdy
nie wprowadzono odpowiednich ograniczeń, mających na celu ochronę
integralności i gdy wartości w wierszach są od siebie zależne. Kiedy w ramach
dwóch (lub więcej) jednoczesnych transakcji odczytywane są i uaktualniane te
same dane, niepożądane efekty uboczne mogą wystąpić wtedy, gdy pierwsza
transakcja kopiuje wartość z jednego wiersza do innego, po czym druga
zmienia tę wartość w pierwszym wierszu. Taki typ załamań integralności
określany jest terminem transakcji przeplatanych (ang. interleaved
transactions).
Sposób rozwiązywania powyższych problemów przez poziom TIL (który
wybieramy z
wykorzystaniem własności
TransIsolation
komponentu
TDatabase
), zależy od platformy RDBMS i poziomu TIL serwera (oba aspekty
omawiamy w poniższych podrozdziałach).
Zarządzanie transakcjami z wykorzystaniem TDatabase
Zarządzając transakcjami poprzez komponent
TDatabase
korzystać będziemy
z własności
TransIsolation
oraz metod:
StartTransaction
,
Commit
i
RollBack
.
TransIsolation
pozwala ustalić poziom izolowania transakcji w serwerze
bazy danych dla konkretnego połączenia. Jak już wspominaliśmy, poziom TIL
używany dla każdego połączenia w serwerze steruje dostępem innych transakcji
współbieżnych do wprowadzonych przez nas modyfikacji, a także zdolnością
naszej transakcji do obserwowania modyfikacji, wprowadzonych w innych
transakcjach współbieżnych.
TransIsolation
może przyjmować jedną z trzech wartości:
tiDirtyRead
,
tiReadCommitted
oraz
tiRepeatableRead (
domyślną jest
tiRead
Committed):
tiDirtyRead
- widoczne są nie zatwierdzone modyfikacje, wprowadzone
w ramach innych transakcji
tiReadCommitted
- widoczne są tylko modyfikacje zatwierdzone,
wprowadzone w ramach innych transakcji
tiRepeatableRead
- nie są widoczne modyfikacje, dokonane przez inne
transakcje w
danych poprzednio odczytanych, co oznacza, że przy
każdorazowym odczycie rekordu w transakcji, zwracany jest dokładnie ten sam
rekord.
Metoda
StartTransaction
komponentu
TDatabase
zaznacza początek
transakcji, czyli grupy modyfikacji (bazy danych), którą chcemy wydzielić jako
652
Część IV
całość. Do bazy danych zostaną wprowadzone wszystkie te modyfikacje, albo
żadna z nich.
Metoda
Commit
zatwierdza modyfikacje bazy danych, które wystąpiły od chwili
rozpoczęcia transakcji. Możemy ją traktować jak komendę
save
(zachowaj)
bazy danych.
Rollback
unieważnia zmiany wprowadzone do bazy danych od momentu
uruchomienia transakcji. Możemy ją traktować jak komendę
undo
bazy danych.
Własność
TransIsolation a
p
oziomy izolowania transakcji w DBMS
Poziomy TIL, obsługiwane przez własność
TransIsolation
komponentu
TDatabase,
mogą być inne w naszym serwerze bazy danych, bądź też w ogóle
przez niego nie obsługiwane. Kiedy jeden z żądanych przez nas poziomów
własności
TransIsolation
nie jest obsługiwany przez serwer, przekazywany
jest do następnego, wyższego poziomu izolowania transakcji.
Tabela 23.1. Własność
TransIsolation
a
poziomy izolowania transakcji
w różnych platformach DBMS
Wartość własności
TransIsolation
InterBase
Oracle Sybase
i Microsoft
tiDirtyRead
odczyt
zatwierdzonych
(read committed)
odczyt
zatwierdzonych
(read committed)
odczyt
zatwierdzonych
(read committed)
tiReadCommitted
odczyt
zatwierdzonych
(read committed)
odczyt
zatwierdzonych
(read committed)
odczyt
zatwierdzonych
(read committed)
tiRepeatableRead
odczyt
powtarzalny
(repeatable read)
błąd odczytu
powtarzalnego
(TYLKO DO
ODCZYTU)
(poziom
nieobsługiwany)
(repeatable read
(READ ONLY)
Error)
Sterowanie transakcjami z wykorzystaniem języka SQL
Przetwarzaniem transakcji przez serwer można również sterować za pomocą
języka Passthrough SQL. W tym celu należy posłużyć się komendami SQL, które
Rozdział 23 Sterowanie współbieżnością
653
służą do zmiany sposobu przetwarzania transakcji w serwerze. W poniższych
przykładach użyliśmy składni SQL w wersji InterBase.
UWAGA:
Pamiętajmy, że jeśli za pomocą
SQLPASSTHRUMODE
ustawiliśmy poziom
izolowania transakcji na
SHARED AUTOCOMMIT
lub
SHARED NOAUTO-
COMMIT
, to nowy poziom TIL może wpłynąć (mimowolnie) na inne transakcje
zainicjowane przez naszą aplikację.
Izolowanie transakcji
Aby ustawić, pracując za pomocą SQL, żądany poziom TIL, należy skorzystać
z komendy
SET TRANSACTION
. Np. obsługiwane w systemie InterBase trzy
poziomy TIL to
SNAPSHOT
,
SNAPSHOT TABLE STABILITY i
READ
COMMITTED
(opisane poniżej). W
celu zainstalowania jednego z
nich
powinniśmy użyć komendy SQL
SET TRANSACTION ISOLATION LEVEL
.
SNAPSHOT
: Baza danych przyjmuje postać obrazu statycznego (snapshot) -
w odniesieniu do momentu rozpoczęcia transakcji. Modyfikacje dokonane
przez inne aktywne transakcje nie są widoczne.
SNAPSHOT TABLE STABILITY
: Zablokowanie tabel, odczytywanych lub
zapisywanych w ramach danej transakcji, z umożliwieniem odczytu (tzn.
dostępu typu read-only) innym transakcjom.
READ COMMITTED
: w bazie danych widoczne są ostatnio zatwierdzone
wersje istniejącego wiersza (rekordu), przy czym można wprowadzać takie
modyfikacje, które nie spowodują konfliktu z
innymi, współbieżnymi
transakcjami. Dla tego poziomu można określić dwa parametry:
♦
NO RECORD_VERSION
(parametr domyślny): Ukazuje tylko ostatnią
wersję wiersza (czyli rekordu). Jeśli dla komendy
SET TRANSACTION
podamy parametr
WAIT
, wówczas transakcja zaczeka na zatwierdzenie lub
wycofanie ostatniej wersji rekordu, po czym ponownie dokona odczytu.
♦
RECORD_VERSION
: Odczytuje ostatnią zatwierdzoną wersję wiersza -
nawet jeśli w bazie danych znajduje się nowsza, nie zatwierdzona wersja.
Izolowanie transakcji na serwerze. Klasyczne problemy izolowania
transakcji
Metody rozwiązywania klasycznych problemów izolowania transakcji zależne są
od - obsługiwanego na serwerze - poziomu TIL (tablica 23.3).
654
Część IV
Tabela 23.2. Poziomy izolowania transakcji w InterBase
Poziom TIL
Problem
Rozwiązanie
SNAPSHOT
(obraz statyczny
)
zgubione aktualizacje W innych transakcjach nie można
uaktualniać wierszy, które są
uaktualniane w ramach danej
transakcji
pozorne odczyty
Nie można odczytywać modyfikacji
dokonanych w ramach innych
transakcji; w innych transakcjach
widoczna jest poprzednia wersja
wiersza uaktualnionego w ramach
danej transakcji
niepowtarzalne
odczyty
Możliwość odczytywania tylko tej
wersji wiersza, którą zatwierdzono
przy rozpoczęciu transakcji
wiersze-widma
Możliwość odczytywania tylko tej
wersji wiersza, którą zatwierdzono
przy rozpoczęciu transakcji
efekty
uboczne
aktualizacji
Nie można odczytywać modyfikacji
dokonanych w ramach innych
transakcji; w innych transakcjach
widoczna jest poprzednia wersja
wiersza uaktualnionego w ramach
danej transakcji
READ
COMMITTED
(odczyt
zatwierdzonych)
zgubione aktualizacje W innych transakcjach nie można
uaktualniać wierszy, które są
uaktualniane w ramach danej
transakcji
pozorne odczyty
W innych transakcjach widoczna
jest poprzednia lub zatwierdzona
wersja wiersza uaktualnionego
w ramach danej transakcji
niepowtarzalne
odczyty
dozwolone z definicji
wiersze-widma
wiersze-widma
mogą się pojawić,
ponieważ na tym poziomie TIL
widoczne są zatwierdzone
modyfikacje, wprowadzone
Rozdział 23 Sterowanie współbieżnością
655
Poziom TIL
Problem
Rozwiązanie
w innych transakcjach
efekty
uboczne
aktualizacji
W innych transakcjach widoczna
jest poprzednia lub zatwierdzona
wersja wiersza uaktualnionego
w ramach danej transakcji
SNAPSHOT
TABLE
STABILITY
(stabilność obrazu
statycznego tabeli)
zgubione aktualizacje Inne transakcje nie mogą
uaktualniać tabel, których dotyczy
ten poziom
pozorne odczyty
Inne transakcje nie mają dostępu do
uaktualnianych tabel, których
dotyczy ten poziom
niepowtarzalne
odczyty
Możliwość odczytywania tylko tej
wersji wiersza, którą zatwierdzono
przy rozpoczęciu transakcji; inne
transakcje nie mają dostępu do
uaktualnianych tabel, których
dotyczy ten poziom
wiersze-widma
Inne transakcje nie mają dostępu do
tabel, których dotyczy ten poziom
efekty
uboczne
aktualizacji
Inne transakcje nie mogą
uaktualniać tabel, których dotyczy
ten poziom
Wybór odpowiedniego poziomu izolowania transakcji
Jak widać, prawie każdy aspekt izolowania transakcji wiąże się (w taki czy inny
sposób) z wszystkimi (trzema) poziomami izolowania transakcji w InterBase.
Poziom, który powinniśmy wybrać, zależy w dużym stopniu od wymagań naszej
aplikacji.
SNAPSHOT
(obraz statyczny)
to domyślny poziom TIL. Dla większości aplikacji
należy zastosować właśnie poziom
SNAPSHOT
albo
READ COMMITTED
(odczyt
zatwierdzonych).
Zastosowanie poziomu
SNAPSHOT TABLE STABILITY
może - w nieokreślony bliżej sposób - zablokować dostęp innym użytkownikom do
potrzebnych im tabel. Stąd powinniśmy go raczej unikać.
Decydując się na wybór pomiędzy poziomami
SNAPSHOT
a
READ COMMITTED
powinniśmy odpowiedzieć sobie na pytanie, czy w trakcie działania naszej
656
Część IV
transakcji mają być widziane zatwierdzone aktualizacje, przeprowadzane
w
ramach innych transakcji. Jeśli nie mają, powinniśmy wybrać poziom
SNAPSHOT.
W
przeciwnym razie
- READ COMMITTED
. Ogólnie rzecz
biorąc, z poziomem TIL
READ COMMITTED
wiąże się mniejsza liczba konfliktów
blokowania (lock contention).
Sterowanie transakcjami
Sterowanie transakcjami w InterBase przebiega w oparciu o następujące komendy
języka SQL:
SET TRANSACTION
,
COMMITT
i
ROLLBACK
.
SET
TRANSACTION
ma wiele zastosowań, a
jednym z
nich jest (jak już
wspominaliśmy) ustawianie poziomu izolowania transakcji. Działanie komendy
COMMIT
jest bardzo podobne do metody
Commit
własności
TDatabase -
funkcjonuje więc
jak komenda
save
(Zachowaj) bazy danych. Analogicznie,
komenda
ROLLBACK
funkcjonuje jak metoda
Rollback
tej własności -
unieważnia modyfikacje wprowadzone do bazy danych od chwili wystąpienia
ostatniej komendy
COMMIT
.
Komenda SET TRANSACTION
Komendę tą stosujemy do rozpoczęcia transakcji, np.
SET TRANSACTION
Jeżeli chcemy rozpocząć transakcję typu tylko do odczytu (read-only), musimy
dodać opcjonalne słowo kluczowe
READ ONLY
, np.
SET TRANSACTION READ ONLY
Wiele platform RDBMS obsługuje również transakcje nazwane (named
transactions). Umożliwia to m.in. zagnieżdżanie transakcji. W języku InterBase
SQL komendy do modyfikacji danych (takie jak
INSERT
,
UPDATE
i
DELETE
)
mogą się bezpośrednio odwoływać do transakcji nazwanych. Oto składnia
przykładowego wyrażenia w InterBase, poprzez które uruchamiamy transakcję
nazwaną:
SET TRANSACTION :UpdateCustomers
Zauważmy, że zmienna
:UpdateCustomers
musi być wcześniej
zadeklarowana i zainicjowana.
A oto składnia wyrażenia w języku Sybase Transact-SQL, wpisanego w tym
samym celu:
BEGIN TRANSACTION UpdateCustomers
Rozdział 23 Sterowanie współbieżnością
657
Komendy COMMIT i ROLLBACK
Komenda
COMMIT
języka SQL powoduje utrwalenie modyfikacji, dokonanych
w trakcie danej transakcji. Zachowuje ona modyfikacje, które wprowadziliśmy do
bazy danych i zapewnia, że będą one widoczne dla innych użytkowników.
Z drugiej strony komenda
ROLLBACK
unieważnia wszystkie modyfikacje, które
dana transakcja mogła wprowadzić do bazy danych. W większości systemów
usuwa ona po prostu wszystkie modyfikacje - oczekujące na zatwierdzenie
i
wprowadzone do protokołu transakcji lub wycofań. Obydwie z
wyżej
wymienionych komend wpływają na modyfikacje, które wystąpiły od ostatniej
komendy
COMMIT.
Nie można natomiast wycofać (rollback) modyfikacji już
zatwierdzonych.
UWAGA:
Między komendami
SET TRANSACTION a COMMIT
lub
ROLLBACK
powinno
umieszczać się tylko komendy bezpośrednio związane z modyfikacją danych.
Przeglądanie i inne czynności związane z odczytywaniem informacji powinny być
przeprowadzane przed zastosowaniem komendy
SET TRANSACTION
. Takie
podejście zapewnia możliwie najkrótszy czas blokady, w którym inni użytkownicy
nie będą mieli dostępu do zasobów bazy danych.
Edytor WISQL, udostępniany przez InterBase, automatycznie rozpoczyna
transakcję (poprzez wydanie komendy, będącej odpowiednikiem InterBase
SET
TRANSACTION
) przy pierwszym uruchomieniu. Tuż przed zakończeniem pracy
z WISQL zostaniemy zapytani o to, czy chcemy zatwierdzić efekty naszej pracy.
Możemy je w każdej chwili zatwierdzić lub wycofać - opcją
Commit Work
lub
Rollback Work
- z menu
File
edytora WISQL.
Systemy sterowania współbieżnością
System sterowania współbieżnością możemy np. odnieść do zarządzania
magistralą kolejową. Gdyby jeździł po niej tylko jeden pociąg, to w ogóle nie
byłoby problemu ze współbieżnością; jeżeli jednak pojawi ich więcej, trzeba
szybko opracować jakiś plan, który zapobiegnie kolizjom a jednocześnie zapewni,
że towary będą odbierane i ładowane na czas. Takie same problemy napotykamy
przy bazach danych. Musimy znaleźć sposób zapobiegania ,,kolizjom’’ (tzn.
wzajemnemu nadpisywaniu) aktualizacji wprowadzanych przez poszczególnych
użytkowników, przy zachowaniu oczekiwanej przez nich wydajności.
Oczywiście nie będzie problemów ze współbieżnością, jeśli do bazy danych
kierować będziemy jedynie zapytania. Pojawiają się one dopiero wówczas, gdy
dwóch lub więcej użytkowników zechce zmodyfikować te same dane w tym
658
Część IV
samym czasie. Kiedy modyfikacje jednego użytkownika zostają zgubione lub nie
może on ich wprowadzić z powodu modyfikacji przeprowadzanych przez innego
użytkownika - wtedy mówimy, że wystąpiła zgubiona aktualizacja (lost update)
lub konflikt aktualizacji (update conflict). Efektywne systemy sterowania
współbieżnością rozwiązują te problemy z
zachowaniem użyteczności
i funkcjonalności bazy danych.
Wyróżniamy dwa podstawowe typy systemów sterowania współbieżnością:
pesymistyczny i
optymistyczny. Często określa się je jako systemy
optymistycznego i pesymistycznego blokowania (locking) zasobów bazy danych,
gdyż właśnie blokowanie wykorzystywane jest najczęściej przy rozwiązywaniu
problemów współbieżności. Systemy te rozróżnia się według leżących u ich
podstaw założeń, odnoszących się do prawdopodobieństwa konfliktu, czyli próby
modyfikacji tych samych danych w kilku transakcjach współbieżnych w tym
samym czasie. W pesymistycznym systemie sterowania współbieżnością zakłada
się prawdopodobieństwo takiego konfliktu, podczas gdy w
systemie
optymistycznym uważa się go za zdarzenie nienormalne i traktuje jako wyjątek.
W
optymistycznym systemie sterowania współbieżnością przyjmuje się, iż
większość zapytań jest typu tylko do odczytu (read-only) i że aktualizacje
konkretnej danej w tym samym czasie dokonywane są rzadko.
Pesymistyczne sterowanie współbieżnością
Konsekwencją przyjętego - w pesymistycznym systemie sterowania współbież-
nością - dużego prawdopodobieństwa występowania konfliktów aktualizacji jest
blokowanie dostępu do zasobów wykorzystywanych w danej transakcji. Inne
transakcje, które potrzebują tych zasobów, muszą zaczekać, aż transakcja
blokująca zakończy przetwarzanie.
Chociaż pesymistyczny system sterowania może korzystać zarówno z blokad
odczytu (read-inhibit lock), jak i blokad zapisu (write-inhibit lock), w praktyce
stosowane są jedynie te ostatnie. Zwykle mamy do czynienia z blokadami, które
zezwalają tylko na odczyt danych, lub takimi, które zezwalają zarówno na ich
odczyt, jak i zapis. Dostęp do danych można blokować na różnych poziomach - od
wielu tabel na raz do pojedynczych kolumn.
Najczęściej stosuje się (platforma SQL Server) blokowanie tabel, wierszy i stron.
Blokady takie mogą czasem wystąpić jedna po drugiej. Oznacza to, że po
blokadzie kilku wierszy może wystąpić blokada całej, zawierającej je tabeli.
Nazywamy to eskalacją blokad (lock escalation). Występuje ona wówczas, gdy
serwer bazy danych oceni, że liczba już istniejących blokad usprawiedliwia
zastąpienie ich blokadą całej tabeli.
Rozdział 23 Sterowanie współbieżnością
659
W praktyce nigdy nie spotykamy się z blokadami indywidualnych kolumn;
obciążają one zasoby i
nie służą żadnemu konkretnemu celowi. Rzadko
uzasadnienie znajduje zapobieganie modyfikacji kolumny przecinającej się
z wierszem, który nie jest uaktualniany w innej transakcji.
Ogólnie - z im większą precyzją blokujemy dostęp do bazy danych, tym więcej
użytkowników może go do niej jednocześnie uzyskać. Z kolei - im więcej
korzystających jednocześnie z bazy użytkowników, tym większa staje się liczba
blokad. Jeżeli zablokowaliśmy tylko jedną tabelę, mamy tylko jedną blokadę.
Jeżeli jednak zablokujemy wiele wierszy w wielu tabelach, to wystąpi wiele
blokad. Im więcej blokad, tym więcej zasobów serwera musi zostać
wykorzystanych do zarządzania blokadami i tym dłuższy staje się czas konieczny
do ich zakładania i usuwania. Podobnie jak w wielu innych przypadkach, tak i tutaj
- większa funkcjonalność oznacza również zwiększone obciążenie zasobów.
Z powodu naturalnej tendencji do blokowania całych tabel, pesymistyczne systemy
sterowania podatne są na zatory (deadlock; przyp.tłum**. prawidłowym polskim
tłumaczeniem słowa deadlock w programowaniu współbieżnym jest termin
blokada, tutaj jednak ma on inne znaczenie, i dlatego zdecydowano się tłumaczyć
je jako zator). Zator występuje wówczas, gdy transakcja nr 1 blokuje tabele
wymagane w transakcji nr 2 i odwrotnie. Ponieważ w żadnej z tych transakcji nie
można zablokować tabel, które są potrzebne do jej zakończenia, obie pozostają
zamknięte niczym w śmiertelnym uścisku, i żadna z nich nigdy się nie skończy bez
interwencji z zewnątrz.
Z reguły w serwerach baz danych realizuje się pesymistyczną strategię sterowania.
Jest to zaszłość z czasów, gdy żadne zasoby nie istniały na poziomie stacji
roboczej, z
którego to powodu nie można było stosować mechanizmów
optymistycznych. Ponieważ w
samym Delphi przyjęto właśnie strategię
optymistyczną, w pracy z naszymi aplikacjami możemy zetknąć się z dwiema
strategiami.
Większość serwerów baz danych automatycznie blokuje za nas odpowiednie
elementy danych w trakcie wprowadzania modyfikacji do bazy. Kiedy np.
modyfikujemy wiersz, większość serwerów blokuje go, aby uniemożliwić innym
użytkownikom jego aktualizację w tym samym czasie. Serwery pracujące na
niektórych platformach mogą zablokować całą tabelę, jeżeli usuwamy dużą liczbę
wierszy.
Zarówno w Sybase, jak i Microsoft SQL Server przyjęto blokowanie całych tabel
i stron, a nie pojedynczych wierszy, co z kolei ma miejsce w platformach takich
jak Oracle. Kiedy próbujemy zmodyfikować dany wiersz, strona, na której się on
znajduje, zostaje zablokowana na czas uaktualniania. Aktualizacje zwykle
występują w grupach dotyczących wierszy zgromadzonych w danym obszarze
bazy danych. Jeśli przyjąć blokowanie tych wierszy według stron, zwykle jedna
660
Część IV
blokada wystarcza dla kilku wierszy. Zmniejsza to obciążenie zasobów serwera,
ale może uniemożliwić dostęp do wierszy, których dana aktualizacja nie dotyczy.
W takiej strategii szczególny problem sprawiają aktualizacje pojedynczych
wierszy. Pomijając kwestie opłacalności, podejście przyjęte przez firmę Microsoft
w platformie SQL Server okazuje się w praktyce równie dobre, co przyjęte
w platformach innych producentów DBMS.
UWAGA:
Ostatnio w platformie Microsoft SQL Server pojawiła się możliwość blokowania
wierszy w pewnych, dość rzadkich, sytuacjach, ale udogodnienie to działa
znacznie gorzej, niż obsługa blokowania wierszy zrealizowana w systemie Oracle.
Również w Sybase testuje się obecnie mechanizm blokowania wierszy, który
jednak nie jest jeszcze dostępny na rynku.
Optymistyczne sterowanie współbieżnością
W systemach optymistycznego sterowania współbieżnością zakłada się, że
aktualizacje przeważnie nie wpadają we wzajemne konflikty i że większość
użytkowników bazy danych odczytuje informacje, ale ich nie uaktualnia. Poniższy
przykład powinien pomóc w zrozumieniu takiego systemu. Wyobraźmy sobie
głównego redaktora nadzorującego redagowanie rękopisów książek, które mają
zostać opublikowane. Kiedy otrzymuje on rękopis, powiela go i
rozsyła
podwładnym, którzy z kolei dodają do swoich kopii poprawki i zwracają je.
Następnie redaktor główny musi zintegrować wszystkie modyfikacje i wprowadzić
je do oryginału. Ci redaktorzy, którzy otrzymają poprawiony rękopis, nie muszą
dzięki temu powtarzać wykonanej już pracy.
Powyższy przykład z kilku powodów dobrze ilustruje działanie optymistycznego
systemu sterowania współbieżnością. Po pierwsze zauważmy, że oryginał rękopisu
nigdy nie opuszcza głównego redaktora. Podobnie w systemach optymistycznego
sterowania najpierw modyfikuje się kopię danych. Sam serwer wprowadza
modyfikację w taki sposób, żeby nie nadpisała (overwrite) ona modyfikacji innych
użytkowników.
Zauważmy również naturalny podział modyfikacji, wprowadzanych przez
poszczególnych redaktorów. Jeden z nich sprawdza np. rękopis pod względem
ortografii, inny może być odpowiedzialny za poprawność merytoryczną i tak dalej.
Z reguły dwóch redaktorów nigdy jednocześnie nie redaguje tego samego rękopisu
pod tym samym względem. Oto podstawowe założenie każdego optymistycznego
systemu sterowania współbieżnością. Aktualizacje bazy danych przedsiębiorstwa
dzieli się zwykle według oddziału, poziomu zarządzania lub innych kryteriów.
Rozdział 23 Sterowanie współbieżnością
661
Sprawia to, że system optymistyczny działa dobrze, ponieważ zasoby bazy danych
nie są początkowo blokowane.
Optymistyczne systemy zarządzania współbieżnością w Delphi
Optymistyczne sterowanie współbieżnością udostępniane jest w Delphi przez
własność
UpdateMode
komponentów
TTable
oraz
TQuery DataSet
.
Podobnie jak w innych optymistycznych systemach współbieżnych, Delphi
otrzymuje z serwera kopię wiersza, udostępnia ją nam do modyfikacji, po czym
przesyła mu ją z powrotem. Przekaz ten realizujemy poprzez komendę
UPDATE
w SQL. Komenda w przedstawionej tutaj postaci zawiera klauzulę
WHERE
, służącą
wyszukaniu wiersza do modyfikacji. Poprzez
UpdateMode
definiowane są te
kolumny tabeli, które mają zostać uwzględnione przez klauzulę
WHERE
.
Własność
UpdateMode
może przyjmować jedną z trzech wartości:
upWhereAll
(wartość domyślna) - Klauzula
WHERE
uwzględni każdą
kolumnę w tabeli.
upWhereChanged
- Klauzula
WHERE
uwzględni kolumny według klucza
z
DataSet
oraz kolumny, które uległy zmianie.
upWhereKeyOnly
-
Klauzula
WHERE
uwzględni tylko kolumny według
klucza z
DataSet
(tego ustawienia należy używać tylko wówczas, gdy mamy
wyłączność do tabeli bazy danych).
Obydwa komponenty -
TTable
i
TQuery -
propagują własność
UpdateMode
.
Jej ustawieniem domyślnym jest
upWhereAll
, co oznacza, że uaktualnienie
wiersza w tabeli spowoduje wygenerowanie klauzuli
WHERE
, wyświetlającej
wszystkie jej kolumny. Może to okazać się bardzo kłopotliwe, zwłaszcza
w przypadku dużych tabel. Alternatywne i szybsze rozwiązanie polega na nadaniu
własności
UpdateMode
wartości
upWhereChanged
. W
wyniku takiego
ustawienia generowana będzie klauzula
WHERE
, która uwzględni jedynie kluczowe
i zmodyfikowane pola tabeli.
Przypuśćmy, że nasza aplikacja w Delphi zmodyfikowała już pole
LastName
tabeli CUSTOMER. Poniżej zamieściliśmy kod SQL, wygenerowany przy
ustawieniu
upWhereAll
(zwróćmy uwagę na długość klauzuli
WHERE)
:
UPDATE CUSTOMER
SET LastName=’newlastname’
WHERE CustomerNumber=1
AND LastName=’Doe’
AND FirstName=’John’
AND StreetAdress=’123 SunnyLane’
AND City=’Anywhere’
AND State=’OK.’
AND Zip=”73115’
662
Część IV
A oto kod wygenerowany przy ustawieniu
upWhereChanged
:
UPDATE CUSTOMER
SET LastName=’newlastname’
WHERE CustomerNumber=1
AND LastName=’Doe’
Zauważmy, o ile krótszy jest drugi listing. Zwróćmy również uwagę na to, że
dzięki umieszczeniu w klauzuli
WHERE
oryginalnej wartości pola
LastName
,
w drugim przypadku uniknięto możliwości nadpisania (overwrite) ewentualnej
modyfikacji tego pola, wprowadzonej przez innego użytkownika.
Jeżeli inny użytkownik zmodyfikuje pole
LastName
w czasie, jaki upłynął
między odczytem wiersza a uaktualnieniem go przez bieżącego użytkownika,
komenda
UPDATE,
wygenerowana w efekcie ustawienia
upWhereChanged,
zakończy się niepowodzeniem, i o to nam właśnie chodzi. Oczywiście jeszcze
większą odporność na nieoczekiwane konflikty daje ustawienie
upWhereAll
.
Inny użytkownik mógłby usunąć wiersz po przeczytaniu go przez naszą aplikację,
a potem dodać nowy rekord do tabeli, który przez przypadek może mieć ten sam
klucz i tę samą wartość
LastName
, co poprzedni rekord. Jeśli nasz rekord użyłby
swojej komendy
UPDATE
, to uaktualniłaby ona niewłaściwy rekord. Taki
scenariusz jest jednak bardzo mało prawdopodobny.
Inna wartość
UpdateMode
, a mianowicie
upWhereKeyOnly
, jeszcze bardziej
redukuje odporność na konflikty. Ponieważ wówczas sprawdza się wartości klucza
tylko dla tego pola, które modyfikujemy, nie uwzględnia się jego ewentualnej
modyfikacji przez innego użytkownika - w czasie, jaki upłynął od pierwszego
odczytu rekordu. Pominięcie takiej możliwości może być bezpiecznym
założeniem, ale (tak jak poprzednio), niestety nie zawsze. W
większości
zastosowań z wieloma użytkownikami niebezpiecznie jest zakładać, że inny
użytkownik nie zmodyfikował jeszcze rekordu, który odczytaliśmy w aplikacji
klienckiej. Tak więc optymalizację taką przeprowadzamy w rzadkich przypadkach
i
tylko wówczas, kiedy jest ona niezbędna. Ponieważ klauzula
WHERE,
wygenerowana przez ustawienie
upWhereKeyOnly,
jest z natury rzeczy
krótsza, działa ona szybciej od klauzuli obejmującej dodatkowe kolumny.
Niemniej jednak, przed skorzystaniem z tej opcji powinniśmy skonsultować się
z administratorem bazy danych - by nie narazić się na utratę danych.
W praktyce powinniśmy się trzymać ustawienia
upWhereAll (
chyba że mamy
konkretny powód, żeby wykorzystać inne). Dobrą alternatywą jest wartość
upWhereChanged -
jeżeli tabela, z którą pracujemy, ma dużo kolumn,
a ustawienie
upWhereAll
istotnie spowalnia pracę.
Rozdział 23 Sterowanie współbieżnością
663
Zarządzanie protokołem transakcji
Zastanówmy się, jak serwery baz danych unikają ciągłego zachowywania
modyfikacji, na wypadek zaniku zasilania lub uszkodzenia sprzętu? Czy serwer
dysponuje czymś w rodzaju listy wycofań? Skąd serwer wie, które transakcje mają
zostać unieważnione podczas odtwarzania bazy danych po awarii?
Odpowiedź na wszystkie te pytania jest następująca. Otóż modyfikacje
wprowadzane przez nas do bazy danych są najpierw wprowadzane do protokołu
transakcji (lub protokołu wycofań) bazy danych, a dopiero potem zatwierdzane
w samej bazie, po pomyślnym zakończeniu transakcji. Oznacza to, że protokół
transakcji jest czymś więcej niż tylko protokołem, to jego dotyczą wszystkie
operacje! Tak więc, jeżeli zanik zasilania serwera wystąpił, zanim w bazie danych
zatwierdzono naszą modyfikację, to żadnej zmiany nie trzeba wycofywać, gdyż
w ogóle nie zaistniała ona w tej bazie. Ponieważ nasze nie zatwierdzone
modyfikacje były wprowadzane tylko do protokołu transakcji, oprogramowanie
serwera może w łatwy sposób unieważnić nie zakończoną transakcję, bez
odwoływania się do samej bazy danych.
Jednym z zagadnień, o którym często zapominają twórcy aplikacji baz danych, jest
zarządzanie protokołem transakcji. Zaniedbuje się je zwykle i odkłada na bok, jako
należące do dziedziny administrowania bazami danych.
Istnieje jednak kilka zagadnień związanych z zarządzaniem protokołami transakcji,
z którymi kompetentny twórca oprogramowania dla baz danych typu klient/serwer
musi się zapoznać i które powinien brać pod uwagę w swoich aplikacjach.
Pierwsze z
nich wiąże się z
koniecznością minimalizowania informacji
przechowywanych w protokole transakcji. Drugie - z zaleceniem rozbijania dużych
transakcji na mniejsze, w celu uniknięcia zapełnienia protokołu. Oba zagadnienia
omówiono po kolei w poniższych podrozdziałach.
Minimalizowanie informacji pamiętanych w protokole
Częstym błędem popełnianym przez twórców oprogramowania typu klient/serwer,
którzy wywodzą się ze świata prostych baz danych (tzn. opartych na prostych
plikach, flat files) - takich jak dBASE czy Paradox - jest przeprowadzanie
aktualizacji grupowych z użyciem konstrukcji programowych, a nie języka SQL.
Dobrze ilustruje to poniższy przykład. Przypuśćmy, że musimy zamienić -
z małych na wielkie - litery wszystkich pól nazwisk w tabeli CUSTOMER. Ci
z nas, którzy zdobywali pierwsze doświadczenia pracując z językiem dBASE,
mogliby prawdopodobnie napisać taki oto fragment w języku Object Pascal:
With taCUSTOMER do begin
First;
While not EOF do begin
664
Część IV
FieldByName(‘LastName’).AsString
:=
UpperCase(FieldByName(‘LastName’).AsString);
Next;
end;
end;
Rezultatem takiej strategii jest nie tylko mała szybkość działania, ale też
niepożądane, oddzielne rozpoczynanie i zatwierdzanie transakcji dla każdego
kolejnego wiersza w tabeli. Ponadto, jeśli podczas wykonywania pętli wystąpiłby
jakiś problem, to tylko część wierszy zostałaby uaktualniona - litery w niektórych
nazwiskach zostałyby zamienione na wielkie, a w innych nie uległy zmianie.
Problemowi częściowej aktualizacji można by zaradzić przez wywołanie metody
StartTransaction
naszego komponentu
TDataBase
jeszcze przed
wejściem do pętli. Jednak proces aktualizacji w dalszym ciągu przebiegałby
z niską wydajnością. W każdym, pojedynczym obiegu pętli zostaje wygenerowana
oddzielna instrukcja
UPDATE
języka SQL, zakończona swoją własną klauzulą
WHERE
, której celem jest odnalezienie kolejnego wiersza. Przy bardzo dużej tabeli
CUSTOMER proces ten może ciągnąć się niezwykle długo.
Daleko lepszym podejściem, i to takim, w którym protokół transakcji wykorzystuje
się we właściwy sposób, jest wprowadzenie naszej modyfikacji z wykorzystaniem
komponentu
TQuery
lub
TStoreProc
. Odpowiednik powyższego kodu,
zapisany w języku SQL, wygląda następująco:
UPDATE CUSTOMER
SET LastName=UPPER(LastName)
Ponieważ samo powyższe wyrażenie zostanie potraktowane jako pojedyncza
transakcja, do tabeli zostaną wprowadzone albo wszystkie aktualizacje, albo
żadna. Ponadto nie będą już miały miejsca ciągłe inicjacje i zatwierdzenia
protokołu, w schemacie transakcja po transakcji.
Innym sposobem ograniczenia informacji w protokole, które generuje transakcja,
jest unikanie niekwalifikowanych komend
DELETE
języka SQL. Każdy wiersz do
usunięcia (poprzez
DELETE)
jest najpierw kopiowany do protokołu transakcji tak,
żeby można ją było, w razie potrzeby, unieważnić. W przypadku dużych tabel
można łatwo przekroczyć dopuszczalną wielkość protokołu. Jeżeli chcemy usunąć
wszystkie wiersze tabeli, powinniśmy użyć komendy serwera do opróżniania tabel
(table-truncation command), a nie
DELETE
. Wiele serwerów obsługuje komendę
podobną do komendy
ZAP
języka dBASE, która szybko opróżnia tabelę z jej
zawartości. Przykładowo - w produktach Sybase i Microsoft SQL Server istnieje,
służąca do tego celu, komenda
TRUNCATE TABLE
. Nie tylko działa ona szybciej
od
DELETE
, ale nie jest zachowywana do protokołu transakcji, w związku z czym
nie wiążą się z nią żadne operacje na protokole. Jeżeli nasz serwer nie udostępnia
komendy do opróżniania tabel, mogłoby okazać się, że lepiej jest po prostu
Rozdział 23 Sterowanie współbieżnością
665
,,fizycznie’’ usunąć całą tabelę i ponownie ją utworzyć - w zależności od metody
jej stosowania gdzie indziej w bazie danych i w innych aplikacjach.
Platformy Sybase i Microsoft oferują inny sposób ograniczenia operacji na
protokole, a mianowicie zastąpienie
INSERT SELECT
(przy kopiowaniu wierszy
z tablicy do tablicy) komendą
SELECT INTO
. Wiersze wstawiane za pomocą
INSERT SELECT
przechowywane są w protokole transakcji, stąd też nie
powinniśmy wprowadzać dużych porcji danych. Komendę
SELECT INTO
zrealizowano na platformie SQL Server jako operację nie wiążącą się
z protokołem, gdyż nie powoduje ona dodawania do niego żadnych pozycji.
Składnia powyższa nie będzie obsługiwana na wszystkich platformach DBMS (nie
obsługuje jej np. InterBase). Stąd opisywana technika może okazać się
niewykonalna w konkretnym systemie.
Komenda
SELECT INTO
według składni dialektu Transact-SQL platformy SQL
Server ma następującą postać:
SELECT LastName, FirstName
INTO NEWCUSTOMER
FROM CUSTOMER
Tworzy się tutaj tabelę o nazwie NEWCUSTOMER i wstawia do niej kolumny
LastName
i
FirstName
z tabeli CUSTOMER. Kod powyższy odpowiada
funkcjonalnie następującej składni:
CREATE TABLE NEWCUSTOMER
(LastName CHAR(30),
FirstName CHAR(30))
INSERT INTO NEWCUSTOMER (LastName, FirstName)
SELECT LastName, FirstName
FROM CUSTOMER
Komendę
SELECT INTO
można wykorzystać dla uniknięcia tworzenia pozycji
w protokole transakcji, zgodnie z niżej przedstawionym scenariuszem:
chcemy utworzyć tabelę, a następnie skopiować do niej wiersze z innej tabeli
Wszystkie kolumny w nowej tabeli mają otrzymać wartości z tabeli źródłowej
Nie zależy nam na możliwości wycofania wstawionych wierszy
Podział dużych transakcji na mniejsze
W niektórych sytuacjach musimy przeprowadzić modyfikacje wielkich ilości
danych, prowadzące w
nieunikniony sposób do utworzenia wielu pozycji
w protokole transakcji lub wycofań. Takie aktualizacje mogą spowodować
zapełnienie protokołu i problemy w serwerze. Mogą one również prowadzić do
666
Część IV
blokad, uniemożliwiających innym użytkownikom dostęp do całych tabel. Z tego
powodu dobrze jest rozbić dużą transakcję na mniejsze. Można to przeprowadzić
na kilka sposobów - ten, który wybierzemy, zależy od posiadanej platformy DBMS
i konkretnych potrzeb naszej aplikacji.
Obszerną aktualizację można rozbić na kilka mniejszych, dzieląc poziomo
komendy służące do modyfikacji danych - tzn. rozbijając jedną komendę
UPDATE
lub
DELETE
na kilka komend dotyczących tego podzbioru wierszy, do którego
odnosiła się oryginalna klauzula
WHERE
. Gdyby tabela CUSTOMER
w poprzednim przykładzie miała milion rekordów i chcielibyśmy zamienić litery
w polu nazwiska każdego z nich, moglibyśmy napisać następujący kod:
SET TRANSACTION;
UPDATE CUSTOMER
SET LastName=UPPER(LastName)
WHERE CustomerNumber between 1 and 100000;
COMMIT;
SET TRANSACTION;
UPDATE CUSTOMER
SET LastName=UPPER(LastName)
WHERE CustomerNumber between 100001 and 200000;
COMMIT;
SET TRANSACTION;
UPDATE CUSTOMER
SET LastName=UPPER(LastName)
WHERE CustomerNumber between 200001 and 300000;
COMMIT;
Jest to prosty sposób praktycznej realizacji tak obszernej transakcji.
Inną metodą ograniczenia liczby wierszy, których dotyczy wyrażenie z komendą
UPDATE
lub
DELETE
, jest redukcja tych wierszy, które komenda ta może zmienić
w jednym przebiegu. Nie jest to możliwe we wszystkich platformach DBMS (np.
umożliwia to SQL Server, a już nie InterBase).
W tym celu musimy użyć komendy właściwej dla posiadanego serwera, która
służy do ograniczania liczby wierszy przetwarzanych przez
UPDATE
lub
DELETE
.
Komendę tę możemy powtarzać tyle razy, ile potrzeba dla przetworzenia całej
tabeli. Poniżej zamieściliśmy przykład w języku Transact-SQL systemu SQL
Server.
SET ROWCOUNT 50000 /* Limits the UPDATE to 50000 rows at a
➥
time*/
WHILE (EXISTS (SELECT * FROM ORDERS WHERE Amount<>0))
BEGIN
UPDATE ORDERS SET Amount=0
WHERE Amount<>0/* Keeps the UPDATE from looping infinitely */
Rozdział 23 Sterowanie współbieżnością
667
END
Z uwagi na fakt, iż modyfikowane wiersze ciągle znajdują się w tabeli z każdym
obiegiem pętli, musimy znaleźć sposób ich usunięcia po przetworzeniu. Jednym
z nich może być wykorzystanie klauzuli
WHERE
komendy
UPDATE
- tak, żeby
modyfikowane wiersze były przez tę klauzulę ignorowane po ich zmodyfikowaniu.
Innymi słowy musimy sprawdzać kolumny Amount z użyciem testu <> 0,
ponieważ w komendzie
UPDATE
polu Amount nadajemy wartość 0. Dzięki temu
komenda ta, modyfikując dany wiersz, zaznacza go jako już przetworzony.
Zaznaczanie takie zapobiega dwukrotnemu przetworzeniu wiersza.
A oto przykład z komendą
DELETE
:
SET ROWCOUNT 50000 /* Limits the DELETE to 50000 rows at a
➥
time*/
WHILE (EXISTS (SELECT * FROM ORDERS WHERE Amount=0))
BEGIN
DELETE FROM ORDERS
WHERE Amount=0
END