Gestione delle transazioni
Se i dati
non evolvessero nel tempo, non vi sarebbe alcuna difficoltà a garantire
la consistenza della base di dati. Ma nella maggior parte dei casi i contenuti
di un DB sono dinamici, e nonostante i vincoli d’integrità impongano
regole ferree cui i dati devono sottostare, le transizioni da uno stato
consistente all’altro non avvengono mai in un tempo nullo. Ne consegue
che, nell’intervallo che intercorre tra due stati consistenti, qualche
regola d’integrità sarà necessariamente violata. A causa
di ciò occorre cercare di garantire che durante gli intervalli di
inconsistenza non si presentino condizioni che mettano a rischio la transizione
del DB verso uno stato consistente.
Gli eventi pericolosi
sono molteplici, ma possono essere sostanzialmente raggruppati in due grandi
categorie:
-
malfunzionamenti
hardware e/o software dei sistemi;
-
accesso concorrente
ai dati.
In questo articolo
ci occuperemo solo della seconda categoria, non perchè i malfunzionamenti
hard/soft siano meno importanti, ma solamente per questioni di spazio e
soprattutto perchè i problemi legati all’accesso concorrente sono
quelli che hanno l’impatto più evidente sulla scrittura del codice
applicativo.
Transazioni
Una transazione
è definita come un’unità logica di elaborazione, cioè
una sequenza di operazioni che hanno un effetto globale sul database.
Ciò che
si richiede è che una transazione sia ACID, con ciò intendendo
che essa soddisfi alle seguenti condizioni:
-
Atomicity: o tutte
le operazioni della sequenza terminano con successo (commit) oppure, se
anche una sola di esse fallisce, l’intera transazione viene abortita (abort).
-
Consistency: una
transazione è una trasformazione corretta dello stato del database,
vale a dire, al termine di ogni transazione il DB deve trovarsi in uno
stato consistente.
-
Isolation: l’effetto
di esecuzioni concorrenti di più transazioni deve essere equivalente
ad una esecuzione seriale delle stesse. Quindi, transazioni concorrenti
non devono influenzarsi reciprocamente.
-
Durability: gli
effetti sulla base di dati prodotti da una transazione terminata con successo
sono permanenti, cioè non sono compromessi da eventuali malfunzionamenti
Lo stato di un database
viene fatto evolvere per transazioni e questo significa che una transazione
parte sempre da uno stato consistente e deve comunque terminare lasciando
il DB in uno stato consistente.
Possono quindi
presentarsi due casi:
-
committed transaction:
tutte le operazioni che compongono la transazione sono state eseguite con
successo ed il database si trova in un nuovo stato consistente:
-
aborted transaction:
alcune operazioni della transazione non possono essere portate a termine
correttamente, quindi il DB viene riportato nello stato consistente in
cui si trovava prima dell’inizio della transazione (rollback).
Un apposito modulo
del DBMS chiamato Transaction Manager (TM) ha il compito è quello
di coordinare tutti i moduli che compongono un DBMS affinchè le
transazioni siano di tipo ACID.
Anomalie dovute
all’accesso concorrente
Se le transazioni
venissero sempre eseguite sequenzialmente, il database si troverebbe sempre
in uno stato consistente. Purtroppo, nelle applicazioni reali, più
utenti operano contemporanemente sugli stessi dati, quindi non è
raro che si possano verificare conflitti dovuti alla concorrenza negli
accessi. Se più transazioni si sovrapponessero in modo non controllato,
avremmo di nuovo un problema di incoerenza. In particolare si potrebbero
verificare i seguenti tipi di anomalie:
Anomalie write-write
(lost update - perdite di aggiornamenti): un esempio di questo tipo di
anomalia può essere quello in cui due transazioni (T1, T2)
vengono avviate concorrentemente.
-
la T1 aggiorna un
dato X;
-
la T2 aggiorna a
sua volta lo stesso dato X;
-
la T1 va in abort;
-
il DBMS riporta
il database allo stato precedente l’inizio di T1;
-
la transazione T2
termina con successo;
l’effetto netto
è che l’aggiornamento effettuato dalla transazione T2 viene perduto.
Anomalie read-write
(unrepeatable read - letture non riproducibili)
-
una transazione
T1 legge un dato X;
-
una seconda transazione
T2 aggirona lo stesso dato X e va in commit;
-
la transazione T1
riaccede al dato X (senza averlo modificato);
l’effetto netto
di questa anomalia è che la transazione T1 effettua due letture
dello stesso dato che restituiscono valori diversi, perchè la seconda
lettura è stata inficiata dall’esecuzione concorrente della transazione
T2.
Anomalie write-read
(dirty read - letture improprie o fantasma)
-
una transazione
T1 modifica un dato X
-
una transazione
T2 legge lo stesso dato X
-
la transazione T1
abortisce
l’effetto netto
di questa anomalia è che il dato letto da T2 non è valido,
perchè non definitivo nel databaseLa prevenzione di ciascuno di
questi tipi di problemi dà luogo a diversi livelli di isolamento
(isolation levels), che possono essere così definiti (la nomenclatura
non è del tutto standard):
-
chaos: non viene
tentato alcunchè per prevenire le tre le anomalie citate;
-
browse: si evitano
solo i lost update;
-
cursor stability:
non si hanno nè lost update nè dirty read;
-
repeatable reads:
è l’isolamento che evita tutte le anomalie.
Perchè una
tale distinzione, quando è chiaro che si vorrebbero evitare tutte
le possibili anomalie? La risposta è presto trovata: all’aumentare
del livello di isolamento, diminuisce il grado di concorrenza ottenibile,
quindi, per avere una maggiore flessibilità nel tuning dei sistemi,
in genere i DBMS permettono di specificare il livello di isolamento delle
transazioni.
I lock sui
dati
Ma com’è
possibile garantire i diversi livelli di isolamento? La principale tecnica
è quella del lock dei dati. Essa è in pratica una regola
di buona educazione che tutte le transazioni devono rispettare: prima di
poter utilizzare un dato, una transazione deve richiederne il permesso
al DBMS, tramite un’operazione detta appunto di lock. Una transazione che
richiede un lock rimane in attesa fino a quando il DBMS non concede il
permesso di proseguire. In parole povere, il lock non è altro che
un semaforo per l’accesso ai dati.
Esistono due
tipi di lock:
-
lock in lettura,
che le transazioni richiedono ogni volta che devono svolgere delle operazioni
di lettura
-
lock in scrittura,
che le transazioni richiedono ogni volta che devono compiere operazioni
di scrittura
Perchè due
tipi di lock? Anche in questo caso si tratta di una questione di efficienza.
Viste le anomalie
descritte sopra, è evidente che su un dato che deve essere modificato
non possono essere eseguite concorrentemente nè operazioni di lettura
nè operazioni di scrittura: ne consegue che un lock in scrittura
(detto anche esclusivo) non deve permettere nessun altro tipo di accesso
contemporaneo. Ma perchè inibire completamente l’accesso ad un dato
che deve solo essere letto quando non sono possibili anomalie di tipo read-read?
Proprio per questo motivo, la regola è che un lock in lettura è
compatibile con altri lock in lettura.
Ovviamente,
parlando di lock (e quindi di semafori), si deve necessariamente considerare
la possibilità di dead-lock.
Per definire
una tale condizione di stallo partiamo da un esempio:
-
la transazione T1
richiede ed acquisisce il lock LR(D1) (lock in lettura sul dato D1);
-
la transazione T2
richiede ed acquisisce il lock in lettura LR(D2);
-
la transazione T1
richiede il lock in scrittura LW(D2), che non è compatibile con
il lock in lettura LR(D2) posseduto da T2, e viene messa in attesa dal
transaction manager;
-
la transazione T2
richiede il lock in scrittura LW(D1), che non è compatibile con
il lock in lettura LR(D1) posseduto da T1, e viene messa in attesa dal
transaction manager.
E’ chiaro che T1
e T2 sono in dead-lock, cioè nessuna delle due potrà mai
proseguire nell’esecuzione perchè entrambe sono in attesa di una
risorsa posseduta dall’altra.
Quindi, tutte
le volte che due o più transazioni sono poste in mutua attesa di
risorse bloccate si ha dead-lock.
Il transaction
manager può gestire le situazioni di blocco critico in due modi:
-
adottare una oculata
politica di allocazione delle risorse che eviti in ogni caso le condizioni
di dead-lock;
-
allocare le risorse
in modo non controllato ed eseguire un controllo a posteriori per individuare
eventuali transazioni in dead-lock, e scegliendo quali abortire per liberare
le risorse necessarie al proseguimento dell’esecuzione delle altre.
Conclusioni
Per concludere
occorre considerare che lo standard SQL non prevede un uso esplicito delle
operazioni di lock. A pensarci bene questo non è un limite, ma anzi,
è una garanzia: se si demanda al transaction manager tutto il meccanismo
di locking, si ha la certezza che l’accesso ai dati verrà costantemente
tenuto sotto controllo, senza la possibilità che qualche applicazione
"dimentichi" di eseguire un lock esplicito e comprometta tutto il database. |