Let's Do It Romania - 24 Septembrie 2011



   

Concurenţă şi consistenţă

   

O privire asupra "dedesubturilor" bazelor de date: mecanismele tranzacţionale.


   

În lumea bazelor de date, corectitudinea şi performanţa sunt cerinţe esenţiale dar, din păcate, contradictorii. Pentru a negocia cel mai avantajos compromis trebuie să cunoaşteţi dedesubturile mecanismelor tranzacţionale.

Mircea Sârbu


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.


 

(Publicat în PC Report 60 - septembrie 1997)

 

Copyright © 1997 Agora Media

Creative Commons License
This work is licensed under a Creative Commons License.