Mecanismele tranzacţionale nu sunt o noutate în lumea bazelor de date.
În anii 70, la doar câteva sute de kilometri distanţă, laboratoarele
IBM de la San Jose şi Universitatea Berkeley dezvoltau în paralel primele
sisteme relaţionale, System R şi Ingres, ambele cuprinzând anumite forme
de procesare tranzacţională. Începând de atunci, toate SGBD-urile
importante încorporează mecanisme tranzacţionale.
Procesarea tranzacţiilor are ca scop păstrarea integrităţii bazei
de date. Trebuie însă precizat că mecanismele tranzacţionale
nu sunt singurele care se ocupă de păstrarea integrităţii.
Mai precis, procesarea tranzacţiilor se referă doar la două aspecte:
- Recuperarea bazei de date după un incident (database recovery) -
se bazează pe inducerea unui anumit nivel de redundanţă prin
memorarea istoriei tranzacţiilor într-un aşa-numit "jurnal"
(log). Desi acest aspect nu face subiectul articolului de faţă (un
articol pe această temă, "Evitarea dezastrelor", a apărut în PC Report - octombrie 1993), anumite elemente tehnice privind jurnalizarea vor fi utilizate în continuare.
- Controlul interferenţelor care pot avea loc între tranzacţiile care
se execută în mod concurent (concurrency control) - este
un aspect critic în sistemele de aplicaţii OLTP (On-Line Transaction
Processing). Este vorba despre "controlul" (şi nu neapărat
"evitarea") interferenţelor deoarece, deşi întotdeauna nedorite,
aceste interferenţe pot fi permise - în anumite forme bine precizate
- pentru a creşte performanţele sistemului. Vom vedea în continuare
cum se realizează acest lucru.
Câteva observaţii preliminare:
a. Integritatea (sau consistenta) bazei de date este o noţiune care este destul
de dificil de definit în mod riguros. Pentru scopurile acestei prezentări
este suficient să admitem că baza de date este consistentă
dacă respectă toate regulile formale de integritate (integrity
rules) care i-au fost impuse şi, în plus, informaţiile pe care le conţine
se presupune că sunt corecte.
b. Toate aspectele discutate în continuare se referă la baze de
date de mari dimensiuni, exploatate în regim multi-utilizator. Vom considera
mai ales cazul sistemelor relaţionale (marea majoritate a SGBD-urilor sunt de
acest fel) desi - exceptând aspectele legate de SQL - toate
noţiunile prezentate sunt aplicabile oricărui tip de SGBD. Mai mult,
majoritatea noţiunilor depăşesc în generalitate sfera bazelor de
date, fiind aplicabile sistemelor concurente în general.
c. În cele ce urmează nu voi detalia modul în care este implementat
accesul concurent (time-sharing, multithreading, multiprocessing, etc.). Va
fi considerat în general cazul sistemelor centralizate dar, când
sunt relevante, vor fi amintite şi diferenţele care pot apărea în
cazul sistemelor distribuite.
Trei probleme
În absenţa unor mecanisme de control al concurenţei pot apărea
fenomene nedorite, care pot conduce la rezultate eronate sau la coruperea consistenţei bazei de date. Literatura de specialitate menţionează în general
trei anomalii tipice care pot să apară [DATE95]. Pentru exemplificare
vom considera o aplicaţie de rezervare de locuri pentru o companie de transporturi aeriene (vezi caseta "Exemplu aplicativ").
Să considerăm două tranzacţii de tip "Rezervare", R1 şi R2, care se execută în mod concurent şi care, întâmplător, se referă la acelaşi zbor Z. Vom face abstracţie de operatia de inserare (care nu este importantă în acest context) şi vom folosi o notaţie simplificată: deoarece selecţia este - în esentă -
o citire, o vom nota cu Read. Modificarea, fiind o scriere, o vom nota cu Write.
O posibilă succesiune temporală a evenimentelor ar putea fi următoarea:
t1 - R1: Read Z
t2 - R2: Read Z
t3 - R1: Write Z
t4 - R2: Write Z
...
Citirile efectuate de cele două tranzacţii vor găsi linia în
aceeaşi stare. Să presupunem că valoarea câmpului VINDUT
este 25. Ambele tranzacţii vor seta variabilele lor locale vîndut la valoarea
25. Actualizarea efectuată de tranzacţia R1 la momentul t3 va modifica
valoarea câmpului la 26. La momentul t4, tranzacţia R2 actualizează
şi ea linia Z, plasând însă în câmpul VINDUT
aceeaşi valoare. Se realizează două rezervări, dar numărul
locurilor vândute creşte doar cu o unitate. Dacă numărul
solicitărilor este mare, este foarte probabil că va fi vândut
un loc în plus la cursa respectivă.
Anomalia, numită "modificarea pierdută" (the lost update)
a survenit datorită faptului că tranzacţia R2 şi-a bazat actualizarea
de la momentul t4 pe o valoare citită la momentul t2, care însă
a fost între timp (la momentul t3) modificată de tranzacţia R1.
Practic, efectul actualizării realizate de tranzacţia R1 la momentul
t3 s-a pierdut. Dacă tranzacţia R2 ar fi citit linia Z după ce
ea a fost modificată de R1, atunci R2 ar fi plasat în câmpul
VINDUT valoarea 27, ceea ce ar fi fost corect.
O altă situatie posibilă este cea în care una dintre tranzacţii
este anulată. Să considerăm scenariul următor:
t1 - R2: Read Z
t2 - R2: Write Z
t3 - R1: Read Z
t4 - R2: ROLLBACK
t5 - R1: Write Z
...
În această situaţie, tranzacţia R1 va avea în variabilele
locale rezultatul scris de tranzacţia R2 la momentul t2. Anularea tranzacţiei
R2 poate surveni din motive diverse (să presupunem că adăugarea
liniei corespunzătoare rezervării în tabela REZ eşuează
din lipsă de spaţiu pe disc) şi va provoca revenirea liniei Z la valorile
dinainte de momentul t2. În acest caz, tranzacţia R1 îşi va baza
acţiunile viitoare pe nişte valori care, practic, nu au existat niciodată
în baza de date! Să presupunem că la momentul t1 câmpul
VINDUT al liniei Z conţinea valoarea 25. La momentul t2 această valoare
este incrementată, astfel încât tranzacţia R1 va citi la
momentul t3 valoarea 26. La momentul t4, tranzacţia R2 este anulată şi,
în consecintă, valoarea câmpului VINDUT al liniei Z va fi
adusă la valoarea pe care-o avea înainte de începerea tranzacţiei
R2, deci 25. La momentul t5, tranzacţia va scrie în linia Z valoarea 27.
Ceea ce este, evident, greşit. Anomalia (numită uncommitted dependency)
provine din faptul că tranzacţia R1 a citit rezultate intermediare ale
tranzacţiei R2.
Pentru a exemplifica o a treia anomalie, numită "analiza inconsistentă"
(inconsistent analysis) vom considera tabela CLIENT, asupra căreia se
execută în mod concurent două tranzacţii. Tranzacţia T1
calculează suma totală încasată de la pasagerii cursei
Z, în timp ce tranzacţia T2 transferă o anumită sumă
x din contul unui client C3 în contul unui client C1 (care s-a întâmplat
că au zburat împreună). În mod normal suma totală
ar trebui să fie aceeaşi indiferent în care cont se afla suma x.
Să presupunem că soldurile clientilor C1, C2 şi C3 sunt 200, 100
şi respectiv 300, iar x este 20.
Dar iată ce se poate întâmpla:
t1 - T1: Read C1 (sold = 200, total = 200)
t2 - T1: Read C2 (sold = 100, total = 300)
t3 - T2: Read C3 (sold = 300)
t4 - T2: Write C3 (sold = 280)
t5 - T2: Read C1 (sold = 200)
t6 - T2: Write C1 (sold = 220)
t7 - T2: Commit
t8 - T1: Read C3 (sold = 280, total = 580)
...
Pentru clientii C1, C2 şi C3 totalul ar fi trebuit să fie, desigur,
600. Este evident că suma totală va fi greşită. Spre deosebire
de celelalte două cazuri prezentate, nu mai este vorba despre citirea
unor valori actualizate de o tranzacţie înainte de comiterea acesteia.
T1 nu citeşte nimic în timpul dintre momentul începerii tranzacţiei
T2 şi până la comiterea acesteia. Problema provine din faptul că
T1 a citit unul dintre conturi înainte de transfer iar pe celălalt
după transfer.
În toate cele trei cazuri, anomaliile sunt provocate de interferenţele
dintre tranzacţii care se execută în mod concurent. O metodă
simplă (ba chiar simplistă...) de evitare a acestor interferenţe
ar fi executarea completă a tranzacţiilor în ordinea începerii
lor. În acest caz însă performanţele serverului de date ar
fi afectate în mod inacceptabil. Ar fi ca şi cum într-un mare magazin
cu autoservire clienţii ar fi lăsaţi înăuntru unul câte
unul, fiecare după ce clientul precedent şi-a terminat toate cumpărăturile.
Pentru a echilibra cât mai bine performanţa şi siguranţa, constructorii
de sisteme de gestiune a bazelor de date au dezvoltat mai multe procedee prin
care interferenţele între tranzacţiile concurente să poată
fi controlate. Cel mai răspândit este mecanismul bazat pe blocarea
(locking) unor porţiuni ale bazei de date pentru a interzice altor tranzacţii accesul la datele respective pe durată unor operaţiuni critice efectuate de o tranzacţie. O altă metodă este cea bazată pe aplicarea
unor "mărci de timp" (timestamping) asupra tranzacţiilor şi
a obiectelor implicate în tranzacţii. Ambele procedee (precum şi procedeele
"hibride") pornesc de la premisa pesimistă că interferenţele
nedorite sunt oricând posibile şi chiar probabile, deci se bazează
pe prevenirea lor. Există însă şi o abordare optimistă,
care pleacă de "prezumţia de nevinovăţie" şi care încearcă
doar să depisteze şi să rezolve conflictele în cazul în
care acestea apar. Toate metodele au avantaje şi dezavantaje, fiecare dintre
ele se pretează la anumite aplicaţii şi sunt inacceptabile în cazul
altora.
Blocare
Idea pe care se bazează tehnica blocării este foarte simplă:
o tranzacţie care a început să opereze asupra unor date trebuie
să interzică accesul altor tranzacţii la datele respective până
în momentul în care operaţia se încheie. În acest timp,
datele sunt "ţinute sub cheie" (to lock - a încuia, a zăvorî).
Controlul blocării datelor este asigurat de o componentă a SGBD-ului
numită Lock Manager (LM). În momentul în care o tranzacţie
T doreşte să acceseze un anumit obiect al bazei de date (piesă
de date), va cere componentei LM blocarea obiectului. Dacă obiectul este
blocat de o altă tranzacţie, tranzacţia T va fi pusă în
stare de aşteptare (wait) până când obiectul este eliberat.
Să re-analizăm a doua anomalie prezentată în secţiunea
precedentă (uncommitted dependency). La momentul t1 tranzacţia R2 blochează linia Z. La momentul t3, tranzacţia R1 cere la rândul ei blocarea liniei Z dar, linia fiind deja blocată de R2, este pusă în stare
de aşteptare. La momentul t4, tranzacţia R2 se termină (prin ROLLBACK)
şi eliberează linia Z. Abia acum R1 obţine blocarea liniei Z şi poate
să o acceseze. Datele asupra cărora acţionează acum R1
sunt "curate" (efectele tranzacţiei R2 au fost anulate), deci anomalia
a fost evitată.
Se poate observa cu uşurinţă că această modalitate de blocare
este prea restrictivă. Anomaliile apar doar în cazul actualizărilor
datelor, ceea ce sugerează că o rafinare a tehnicii implică
folosirea a două tipuri de blocări:
- Blocare partajată (share lock sau S lock) - Permite citirea
obiectului dar interzice modificarea acestuia, motiv pentru care mai este
numită "blocare pentru citire" (read lock). Mai multe tranzacţii
pot bloca simultan pentru citire un anumit obiect.
- Blocare exclusivă (exclusive lock sau X lock) - Interzice accesul
altor tranzacţii la obiectul blocat, fie pentru citire, fie pentru modificare.
Blocarea exclusivă este mai "tare" decât blocarea partajată,
fiind folosită doar pentru actualizări, motiv pentru care mai
este numită "blocare pentru scriere" (write lock).
Se poate observa că anumite cereri de blocare asupra unui obiect pot
fi compatibile sau nu, în functie de tipul blocării. Compatibilitatea
cererilor de blocare poate fi exprimată sintetic printr-o matrice de
compatibilitate (vezi caseta "Focus: Blocări").
O situaţie specială este cea în care o tranzacţie blochează
pentru citire un obiect şi doreşte să obţină o blocare exclusivă.
Dacă obiectul respectiv nu este blocat partajat şi de către alte
tranzacţii, blocarea exclusivă este obţinută de tranzacţia în
cauză. Procedeul de numeşte "promovarea blocării" (lock
promotion).
Să ne oprim puţin pentru câteva observaţii:
a. În cazul celor mai multe SGBD-uri, blocarea este implicită:
orice operaţie asupra datelor este automat precedată de cererea pentru
obţinerea blocării datelor respective (S lock pentru citire, X lock pentru
actualizare - întelegând prin "actualizare" operaţiile
UPDATE, INSERT şi DELETE). Încheierea tranzacţiei (cu sau fără
succes) eliberează în mod automat blocările pe care aceasta
le deţinea. Anumite sisteme (de pildă Adabas D) permit "înlănţuirea
tranzacţiilor" (transaction chaining) cu păstrarea anumitor blocări
(prin clauza KEEP LOCK a instrucţiunii COMMIT).
b. Standardul SQL nu face nici o prezumţie legată de modul de implementare
a mecanismelor de control al accesului concurent (prin blocare, prin mărci
de timp sau alte metode) şi, în consecinţă, nu prevede instrucţiuni
explicite de blocare. Cu toate acestea, majoritatea SGBD-urilor relaţionale
oferă în propriile dialecte SQL instrucţiuni de blocare explicită
(LOCK). Anumite sisteme non-relaţionale (Raima, de exemplu) folosesc doar blocări
explicite. De obicei, aceste sisteme prevăd şi instrucţiuni de deblocare
explicită (UNLOCK).
c. Deşi sunt cele mai uzuale, blocările partajate şi exclusive nu sunt
singurele posibile. Unele SGBD-uri (IBM DB2, Sybase SQL Server, Raima db_VISTA)
folosesc o aşa-numită "blocare pentru actualizare" (update
lock), care este un nivel intermediar între blocarea partajată
şi cea exclusivă. Dacă mai multe tranzacţii blochează partajat
(pentru citire) un anumit obiect, una dintre ele (doar una) poate obţine o blocare pentru actualizare (remarcaţi că o blocare exclusivă nu poate
fi obţinută în aceste conditii). De obicei utilizarea blocării
pentru actualizare este posibilă doar în situaţia unui mecanism
de control special (bazat în general pe versiuni multiple ale datelor)
care să avertizeze o altă tranzacţie care solicită o blocare
pentru actualizare că datele au fost modificate.
Deadlock
Să revedem acum problema "actualizării pierdute", folosind
blocările partajate şi exclusive. La momentul t1 tranzacţia R1 solicită
o blocare partajată a liniei Z şi (presupunând că linia
nu era blocată pentru scriere de o altă tranzacţie) o obţine.
La momentul t2, tranzacţia R2 solicită şi ea o blocare partajată
a liniei Z şi o obţine la rândul ei. Ambele tranzacţii blochează
în acest moment pentru citire linia Z. La momentul t3, tranzacţia R1 solicită
blocarea exclusivă a liniei Z, pentru a face o actualizare. Nu obţine
blocarea, deoarece linia este blocată pentru citire de tranzacţia R2,
deci este pusă în stare de aşteptare. Tranzacţia R2 cere şi ea
blocarea exclusivă şi, evident, nu o obţine din motive similare. Nici
una dintre tranzacţii nu poate continua, deoarece fiecare aşteaptă ca
cealaltă să elibereze linia Z.
Această situatie se numeste deadlock sau "interblocare". Este
uşor de verificat că şi în situaţia "analizei inconsistente"
se va ajunge la o interblocare. Rezultatul este că am rezolvat problema
anomaliilor (într-adevăr, acestea nu mai apar) dar am obţinut o
altă problemă, cea a interblocărilor. Rezolvarea noii probleme
cuprinde două aspecte:
1. Prevenirea interblocării. Implică stabilirea unui protocol
de acces la date care să facă imposibilă apariţia situaţiilor
de deadlock. O variantă posibilă este ca fiecare tranzacţie să
blocheze "în bloc" toate liniile de care are nevoie. Alta variantă
este stabilirea unei ordini a obiectelor iar cererile de blocare să fie
rezolvate în conformitate cu această ordine. Ambele metode au dezavantajul
că limitează semnificativ performanţele prin blocarea resurselor
pe durate mai lungi decât cele strict necesare. Pe de altă parte,
prima metodă nu este aplicabilă întotdeauna, deoarece este
posibil ca o tranzacţie să nu cunoască de la început toate
datele de care are nevoie.
2. Detectarea interblocării. Cea mai simplă metodă de detectare
a unei situaţii de deadlock este cea bazată pe un mecanism de time-out:
dacă durata execuţiei unei tranzacţii depăşeşte o valoare prestabilită,
sistemul deduce că a apărut o interblocare. O altă metodă
se bazează pe construirea şi menţinerea dinamică a unui "graf
de aşteptare" (Wait-For Graph). Nodurile acestui graf reprezintă
tranzacţiile care se execută (T1, T2, ..., Tn) iar arcele reprezintă
relaţia de dependenţă (aşteptare): existenţa unui arc de la Ti la Tj
semnifică faptul că tranzacţia Ti aşteaptă ca tranzacţia
Tj să elibereze anumite resurse. Detectarea unei interblocări
revine la detectarea existenţei unui ciclu în graful de aşteptare.
În practică, cel mai adesea se utilizează o mixtură
de tehnici: se impune un protocol de acces care să reducă posibilitatea
interblocării (fără să o prevină total, dar
şi fără să inhibeze semnificativ concurenţă), se
implementează un mecanism care să detecteze interblocările
cele mai uzuale, lăsându-le pe celelalte pe seama unui mecanism
de time-out.
Rezolvarea efectivă a unei interblocări revine la stabilirea unei
"victime" dintre tranzacţiile implicate în deadlock şi anularea
ei (ROLLBACK). După ce interblocarea a fost înlăturată,
tranzacţia poate fi lansată din nou în execuţie.
Observaţii:
a. Desigur, într-o interblocare pot să intervină mai mult
de două tranzacţii (deşi experimentele cu System R au demonstrat că
în realitate astfel de situaţii sunt extrem de rare). În acest caz
este posibil ca mai multe tranzacţii să trebuiască anulate pentru
a debloca execuţia.
b. Cele mai multe sisteme reprogramează automat pentru execuţie tranzacţiile
anulate. Există însă şi sisteme care returnează doar
un cod de eroare aplicaţiei, acesteia revenindu-i sarcina să se ocupe
de rezolvarea problemei.
Serializare
Problema care se pune este: cum putem să ştim dacă execuţia concurentă
a unui grup de tranzacţii este corectă sau nu?
Să începem cu o definiţie: Se numeşte planificare (schedule) oricare
ordine posibilă de executare a operaţiilor grupului de tranzacţii considerat.
O primă constatare este că, dacă presupunem că fiecare
tranzacţie în parte din grup este corectă, atunci orice planificare
serială a tranzacţiilor (una după alta, indiferent de ordine)
este corectă. Deşi afirmaţia este intuitivă, se poate justifica
uşor prin faptul că orice program execută o secvenţă de
tranzacţii (deci niciodată două tranzacţii simultan). Aşadar,
tranzacţiile din grupul considerat sunt executate de programe diferite (sunt
deci independente), ordinea execuţiei lor fiind irelevantă (de regulă
FIFO: first in, first out).
În cazul în care se execută operaţii ale unei tranzacţii
în timp ce execuţia altor tranzacţii nu a fost încă încheiată
avem de-a face cu o planificare intercalată (interleaved), caz în
care este posibil ca execuţia să nu fie corectă (cum a fost cazul
celor trei exemple prezentate la început).
Două planificări ale unui grup de tranzacţii sunt echivalente
dacă vor produce mereu acelaşi rezultat, oricare ar fi starea iniţială
a bazei de date (aceasta înseamnă, de fapt, că operaţiile
conflictuale sunt în aceeasi ordine).
O planificare este serializabilă dacă este echivalentă
cu o planificare serială. Orice planificare serială fiind corectă,
înseamnă că am găsit criteriul de corectitudine căutat.
Execuţia unui grup de tranzacţii este corectă dacă s-a făcut
conform unei planificări serializabile. Să remarcăm că
nici una dintre planificările utilizate ca exemple pentru cele trei anomalii
nu este serializabilă.
O caracterizare mai puţin riguroasă, dar mult mai intuitivă a caracterului
serializabil este următoarea: Oricare ar fi două
tranzacţii A şi B dintr-o planificare a unui grup de tranzacţii concurente,
fie A precede logic pe B, fie B precede logic pe A. Faptul că "A
precede logic pe B" înseamnă că B poate vedea rezultatele
execuţiei tranzacţiei A. Deci, dacă considerăm toate obiectele
văzute de tranzacţia A, atunci B vede toate aceste obiecte fie modificate deja de A, fie în forma în care erau înainte ca modificările să
se producă, dar niciodată ca un amestec de obiecte modificate
şi obiecte nemodificate.
Teoria este, desigur, mult mai bogată (vezi de pildă [OSZU91])
şi specifică mai multi algoritmi de verificare a criteriului pe care
l-am enunţat. Rezultatul cel mai important din perspectivă practică
a acestei teorii este aşa-numita "teoremă a seriabilitătii"
(sau "a blocării în două faze"):
Dacă toate tranzacţiile unui grup de tranzacţii concurente sunt bine
formate şi respectă protocolul de blocare în două faze,
atunci orice planificare legală este serializabilă.
Demonstraţia nu este dificilă (o variantă se bazează pe
graful de precedentă al planificării şi se desfăsoară
prin reducere la absurd). Importante sunt însă condiţiile:
O tranzacţie este "bine formată" dacă orice operaţie
a tranzacţiei este acoperită de o blocare şi toate blocările sunt
eliberate. O operaţie (citire sau scriere) este "acoperită de o
blocare" dacă tranzacţia deţine o blocare suficient de puternică
a obiectului asupra căruia se desfăsoară operaţia.
O planificare este "legală" dacă nu se desfăsoară
în condiţiile unor blocări conflictuale.
Un "protocol de blocare" constă dintr-un set de restricţii
impuse ordinii în care tranzacţiile concurente blochează şi deblochează
obiectele bazei de date. Protocolul de blocare în două faze (two-phase
locking) impune următoarele două restricţii:
1. Înainte de a opera asupra oricărui obiect al bazei de date,
o tranzacţie trebuie să obţină blocarea respectivului obiect.
2. După ce a eliberat o blocare, o tranzacţie nu va mai încerca
niciodată să obtină noi blocări.
Se observă cu usurinţă explicaţia denumirii "în două
faze" (care n-are nimic în comun cu comiterea în două
faze): conform acestui protocol orice tranzacţie trece mai întâi
printr-o fază de obţinere a blocărilor, după care trece
într-o fază de eliberare a blocărilor.
O scurtă discuţie asupra acestui protocol se impune. În primul
rând trebuie spus că sarcina asigurării respectării
acestui protocol revine Lock Manager-ului (desigur, dacă sistemul lucrează
pe baza acestui protocol). Mai observăm că protocolul prevede
"eliberarea blocărilor". O regulă de "bun simţ"
spune că un obiect trebuie blocat cât mai puţin timp cu putinţă,
pentru a permite şi accesul altor tranzacţii la acesta. Din păcate, implementarea acestui protocol în forma sa cea mai eficientă din acest punct
de vedere este extrem de dificilă: LM trebuie să "ştie"
când o tranzacţie a obţinut toate blocările şi nu va mai avea nevoie
de altele (pentru a identifica faza în care se află); LM trebuie
să ştie dacă tranzacţia va mai avea nevoie de un obiect asupra
căruia a efectuat o operaţie, deci dacă poate sau nu să-l elibereze (din acelaşi motiv al identificării fazelor); în fine,
apare problema extrem de dificilă a "anulării în cascadă"
a tranzacţiilor: în cazul în care o tranzacţie a fost anulată
în faza a doua, este posibil ca unele date eliberate de aceasta au fost
apoi blocate de alte tranzacţii, caz în care aceste tranzacţii trebuie
la rândul lor anulate. Din cauza acestor dificultăţi, majoritatea
sistemelor implementează o variantă mai restrictivă a protocolului,
în care toate eliberările blocărilor unei tranzacţii sunt
amânate până la terminarea tranzacţiei (fie prin COMMIT fie
prin ROLLBACK).
Este deci rezonabil să considerăm că blocările obţinute
de o tranzacţie sunt păstrate până la terminarea acesteia,
cu toate că în felul acesta intervalul de timp în care un
obiect este blocat este de regulă mai lung decât minimul necesar.
Compromisul este şi aici prezent: simplificarea mecanismelor implementate în
Lock Manager compensează scăderea nivelului de concurenţă
prin blocări de mai lungă durată. Există însă
situaţii în care se poate admite un grad oarecare de interferenţă,
caz în care nivelul concurentei poate fi crescut prin "relaxarea"
protocolului de blocare (în spetă prin admiterea unor condiţii
în care anumite blocări pot fi eliberate înaintea terminării
tranzacţiei).
Observaţie: Din câte ştiu, toate sistemele relaţionale majore implementează
diverse forme ale protocolului de blocare în două faze. Sistemele
care utilizează doar blocări şi deblocări explicite lasă
implementarea acestui mecanism în seama proiectanţilor de aplicaţie. În oricare
situaţie, proiectanţii sunt sfătuiţi să nu lase controlul blocării
nici o clipă în seama utilizatorilor finali, din motive lesne de
înteles (sindromul "blocării peste pauza de masă").
Blocare ierarhicã
Dacă până acum am folosit în exemple doar blocări
la nivel de articol, în cele ce urmează vom considera şi blocări
aplicate unor portiuni mai mari (pentru a nu complica expunerea, ne putem limita
la nivel de tabelă). Tehnica blocării ierarhice se referă
la sistemele care admit mai multe granulaţii ale blocării (vezi caseta
"Focus: Granulaţie") şi a fost introdusă de Jim Gray şi colaboratorii
săi încă din 1975. O descriere mai "didactică" poate fi consultată în [GRAY93].
Ideea de la care se pleacă este că pentru o obţine o blocare la
nivel de articol (linie de tabelă), o tranzacţie trebuie să-şi
manifeste această intenţie, cerând mai întâi o blocare
la nivel de tabelă. În felul acesta se simplifică procesul
de detectare a unor cereri conflictuale de blocare la nivel de linie, deoarece
ele trebuie întâi să se manifeste la nivel de tabelă.
Protocolul bazat pe "intenţii de blocare" (intent locking) introduce
trei noi tipuri de blocare (la nivel de tabelă), alături de cele
două tipuri de blocări uzuale (S-lock şi X-lock) care, aşa cum
am arătat deja, au sens şi la nivel de tabelă. Avem astfel cinci
tipuri de blocări pe care o tranzacţie T le poate solicita la nivelul
întregii tabele R, prezentate în ordinea crescătoare a "tăriei"
lor relative:
IS - intent shared. Tranzacţia T intenţionează să
blocheze pentru citire (S lock) anumite linii ale tabelei R, pentru a garanta stabilitatea acestora în timpul procesărilor pe care le va efectua.
IX - intent exclusive. La fel ca IS, dar T s-ar putea să
vrea să modifice anumite articole şi deci să solicite blocarea
exclusivă a acestora.
S - shared. Este obişnuita blocare partajată. T admite citiri
concurente ale tabelei R, dar nu şi scrieri. T nu va face nici o scriere la
nivelul liniilor tabelei.
SIX - shared intent exclusive. Combină S şi IX. În
plus faţă de S, T s-ar putea să vrea să modifice anumite
linii din R şi, în consecinţă, să solicite blocarea exclusivă
a acestora.
X - exclusive. T nu admite citiri concurente în tabela R.
T ar putea să actualizeze anumite linii ale tabelei R.
Câteva observatii:
a. Noţiunea de "tărie" a blocării se referă la
faptul că o cerere de blocare de un anumit tip care eşuează, va
eşua cu siguranţă pentru orice tip mai "tare". (vezi "Focus:
blocări").
b. Tipurile IX şi S sunt egale din punct de vedere al "tăriei".
c. Şi aceste tipuri de blocări sunt în mod obişnuit cerute
în mod implicit. Cu toate acestea, sistemele care implementează
aceste tipuri de blocare (DB2, OpenIngres) oferă de obicei şi o instrucţiune
de blocare explicită pentru aceste tipuri.
d. Varianta pe care am prezentat-o este mult simplificată. De fapt blocările
se pot referi la orice granulaţii şi orice obiecte ale bazei de date (inclusiv
indecşi).
În fine, protocolul bazat pe intenţii de blocare (intent locking protocol), impune două reguli care îmbină blocările la nivel
de tabelă şi de articol într-o combinaţie care adesea s-a dovedit
mai eficientă în aplicatii:
1. Înainte de a obţine o blocare partajată (S) la nivel de linie,
o tranzacţie trebuie să obţină o blocare de tip IS (sau mai tare)
la nivelul întregii tabele.
2. Înainte de a obţine o blocare de tip exclusiv (X) la nivel de linie,
o tranzacţie trebuie să obtină o blocare de tip IX sau mai tare
la nivelul întregii tabele.
Ce urmează?
Din păcate spatiul tipografic este limitat iar subiectul este vast.
În articolul următor voi prezenta controlul concurenţei prin mărci
de timp şi prin procedee de tip "optimist". În încheiere
o discuţie asupra nivelurilor de izolare indicate de standardul SQL şi a câtorva
extensii ale acestuia.