Qualunque richiesta ad un DBMS è a tutti gli effetti una transazione. Una transazione può essere interpretata come un insieme di operazioni di lettura e di scrittura sul database e costituisce l'effetto di una serie di funzioni effettuate dagli utenti.
In un database relazionale la modalità standard adottata per l'avvio e la conclusione di una transazione viene chiamata "Non Concatenata" o Autocommit. In questo tipo di approccio ogni query o aggiornamento avvia e conclude una nuova transazione. Questo è il caso di una interrogazione semplice o del richiamo di una certa vista sul DB.
Nel caso in cui abbiamo bisogno di effettuare più operazioni sul DB dipendenti tra loro, quando, ad esempio, il successo di un'operazione può compromettere a cascata quello delle precedenti, dobbiamo definire la transazione in modo più strutturato. Le operazioni devono essere eseguite in un azione atomica, non suddivisibile, bisogna che l'utente avvii e concluda la stessa utilizzando la modalità Concatenata.
Questo tipo di approccio prevede la dichiarazione dell'inizio e della fine del blocco di istruzioni. Con il comando BEGIN TRANSACTION
indichiamo l'inizio della transazione.
BEGIN TRANSACTION [transaction_name]
Il blocco di istruzioni si concluderà poi con l'esito della transazione, che può essere
- positivo in questo caso si usa l'istruzione
COMMIT
e significa che tutte le istruzioni del blocco sono state eseguite con successo - negativo in questp caso si usa
ROLLBACK
, ovvero si riavvolge il nastro fino a far ritornare il database nello stato precedente all'inizio della transazione. Questo si verifica se anche una delle operazioni non va a buon fine.
L'istruzione di COMMIT
ha l'effetto di rendere permanente le operazioni solo quando quest'ultime sono corrette. In caso contrario il sistema le annulla come se l'utente avesse chiamato l'istruzione di ROLLBACK
.
Ecco le situazioni in cui si verifica un ROLLBACK
:
- Errori Logici
- Quando vengono immessi dati errati o violati vincoli di integrità.
- Fallimenti Voluti
- Quando il programmatore la invoca esplicitamente.
- Forzature del Sistema
- Quando il DBMS impone il fallimento di una transazione per sbloccare una situazione di "stallo".
- Crash del Sistema o Guasti dell'hardware
- Quando ci sono problemi relativi all'hardware o c'è un blackout elettrico.
Proprietà delle Transazioni
La base di dati è una "risorsa condivisa" da più applicazioni e quindi è soggetta ad accessi concorrenti. Per rendere una transazione stabile, isolata e consistente essa deve rispettare le seguenti proprietà dette "ACID" (proprietà acide):
- Atomicity - Atomicità
- Dopo essere stata avviata, una transazione può terminare con successo, può abortire o può essere abortita dal DBMS dopo l'esecuzione di tutte o alcune delle sue operazioni. Ogni transazione deve essere atomica (non suddivisibile) e quindi deve eseguire tutte le sue azioni o non deve eseguirne alcuna. Il DBMS controlla la sequenza di azioni registrandole (nel "database log file") in modo che ogni qual volta una transazione deve essere abortita le sue azioni vengono annullate.
- Consistency - Consistenza
- Questa proprietà deve garantire che una transazione non violi i vincoli di integrità. Una transazione rappresenta una trasformazione corretta dello stato del database e quindi al suo termine quest'ultimo deve trovarsi in uno stato consistente senza che si verifichino contraddizioni (inconsistency) tra i dati archiviati.
- Isolation - Isolamento
- Una transazione non può leggere risultati intermedi di altre transazioni e quindi deve sempre "osservare" una base di dati consistente. Questo tipo di proprietà deve garantire che gli effetti di una transazione siano isolati fino al termine della stessa. Per questo ogni transazione deve essere eseguita indipendentemente dall'esecuzione delle altre transazioni.
- Durability - Durabilità
- Nel caso in cui un operazione abbia modificato la base di dati i suoi effetti devono essere permanenti. Questo deve avvenire in tutti i casi anche in caso di errori eccezionali.
L'atomicità e la durabilità sono garantite dal "Reliability/Recovery Manager" (Gestore dell'Affidabilità), l'Isolamento dal "Gestore della Concorrenza" e la Consistenza dal "Gestore dell'integrità a tempo di esecuzione".
Problematiche relative alla Concorrenza
La programmazione concorrente è sempre stato un problema in informatica e si è evidenziato nei sistemi operativi (con i processi che condividono memoria centrale) e nei linguaggi di programmazione (con i thread che condividono la memoria heap).
Le problematiche che si possono verificare in casi del genere sono le seguenti:
- Race Contitions - Corse Critiche
- Un fenomeno di corsa si verifica quando più processi effettuano concorrentemente operazioni di aggiornamento su una stessa risorsa. Per superare questo problema viene ritardata l'esecuzione di operazioni in conflitto imponendo alle transazioni di richiedere dei blocchi (detti lock) per poter effettuare le operazioni di lettura e scittura. Una
porzione di codice, che può essere eseguita solamente da un processo, viene detta "Sezione Critica". Prima di ogni Sezione Critica una tranzazione acquisisce i lock sul "codice critico" per poi rilasciarli quando la Sezione Critica termina. - DeadLock - Morte per Stallo
- In questo tipo di problematica due o più processi che vogliono accedere ad una risorsa condivisa attendono tutti che un processo rilasci il controllo di una Sezione Critica così da bloccarsi vicendevolmente. In questo caso si dice che le transazioni sono poste in "mutua attesa" di risorse bloccate. Per gestire questo problema, si può adottare una
politica mirata di allocazione delle risorse o una allocazione non controllata seguita dall'aborto delle cause, per poter liberare le risorse. - Starvation - Morte per Deprivazione
- In questo tipo di problema un processo non riesce mai ad ottenere le risorse di cui necessita. Questa problematica è dipesa principalmente dalla priorità di accesso alla risorsa che il processo possiede. Può essere evitata con "tecniche di invecchiamento" che prevedono l'aumento progressivo e regolano la "priorità di accesso" del processo.
Il DBMS può trovare una soluzione a queste problematiche nei servizi di sincronizzazione che offre ogni sistema operativo ma non lo fa per due motivi principali:
- Concessione di blocchi esclusivi
- Per ragioni di prestazioni è necessario utilizzare lock di granularità più fine. È necessario distinguere "lock in scrittura" e "lock in lettura" perchè se un dato deve essere aggiornato non si possono concorrentemente eseguire né operazioni di scrittura e né operazioni di lettura. Ne consegue che nelle tecniche di controllo reimplementate da
un database "lock in scrittura" agiscono in modo "esclusivo" senza permettere l'accesso ad un dato da parte di altri, mentre "lock in lettura" permettono altre letture per non inibire completamente l'accesso ad un dato. - Garantire la Durabilità
- Una transazione deve essere riproducibile e annulabile in caso di guasti. Il Reliability/Recovery Manager gestisce l'esecuzione delle operazioni di rispristino.
Quindi, il DBMS, per evitare che si verifichino delle anomalie, deve forzatamente implementare un modulo relativo alla gestione della concorrenza che eseguirà le transazioni garantendo il rispetto delle proprietà acide ed in particolar modo della proprietà dell'isolamento.
Anomalie delle Transazioni in un DBMS
Prima di analizzare la gestione della concorrenza in un DBMS dobbiamo rilevare, più in dettaglio, le possibili interferenze che si possono creare nell'esecuzione di diverse transazioni. Prendendo in considerazione due ipotetiche transazioni T1 e T2 che agiscono sul database avremo le possibili anomalie riguardanti il loro accesso concorrente:
- Write-Write - Lost Update : Perdite di Aggiornamento
-
T1
effettua unUPDATE
del datoY
.T2
effettua nuovamente unUPDATE
dello stesso datoY
.- La transazione
T1
viene abortita a causa di un errore e va inROLLBACK
portando il database allo stato di origine. - La transazione
T2
va inCOMMIT
e termina con successo.
Questo tipo di anomalia causa la perdita dell'aggiornamento della transazione T2.
- Read-Write - Unrepeatable Read : Letture non riproducibili
-
T1
effettua unaSELECT
sul datoY
.T2
effettua unUPDATE
dello stesso datoY
e va inCOMMIT
.- La transazione
T1
rieffettua unaSELECT
nello stesso datoY
senza effettuare una modifica.
In questo caso T1 effettua due
SELECT
che restituiscono valori diversi diY
perchè la transazioneT2
, essendo concorrente, ne ha modificato il risultato durante l'esecuzione. - Write-Read - Dirty Read : Letture Fantasma
-
T1
effettua unUPDATE
sul datoY
.T2
effettua unaSELECT
dello stesso datoY
.- La transazione
T1
abortisce andando inROLLBACK
.
In quest'ultimo caso,il dato letto dalla transazione
T2
non sarà valido perchè non è persistente nel database.
Gestione della Concorrenza
Per gestire queste anomalie bisogna aumentare il livello di isolamento che automaticamente diminuisce e regola il grado di concorrenza che un determinato dato può ottenere. I DBMS prevedono la possibilità di rilasciare i vincoli di isolamento tramite il concetto di "serializzabilità". Il DBMS nel rispettare questo concetto adotta un sistema di lock a livello di record (e non di tabella come nei sistemi operativi). I blocchi adottati sono:
- Shared Locks - Blocchi Condivisi
- Vengono richiesti in caso di lettura di un record e la loro acquisizione può essere concorrente ad altre acquisizioni dello stesso tipo
- Exclusive Lock - Blocchi esclusivi
- Vengono richiesti in caso di scrittura di un record e sono eseguiti in isolamento
Per la gestione delle acquisizioni di queste due tipologie di blocchi il DBMS implementa una strategia di "concessione dei lock" chiamata "Strict Two Phase Locking".
Il protocollo Strict 2PL stabilisce che:
- Ogni transazione deve ottenere uno
Shared Lock
su un oggetto prima di effettuare una lettura - Ogni transazione deve ottenere un
Exclusive Lock
su un oggetto prima di effettuare una scrittura - Se una transazione ha acquisito un
Exclusive Lock
su un oggetto nessuna altra transazione può acquisire Lock di qualsiasi tipo su quell'oggetto. - Ogni Lock acquisito da una transazione deve essere rilasciato al suo termine
Lo Strict 2PL permette solo esecuzioni seriali delle transazioni, snellisce l'esecuzione di aborto delle transazioni e risolve i problemi relativi alle perdite di aggiornamento (Lost Update) e alle letture non riproducibili (Unrepeatable Read).
Il problema relativo alle letture fantasma (Dirty Read) viene risolto adottando un'altra tipologia di Lock che nell'arco della durata di una transazione non permette la modifica del numero degli elementi che soddisfano le condizioni. I Lock vengono adottati sui predicati utilizzati nelle clausole WHERE
e in concreto vengono acquisiti per via della presenza dei livelli di isolamento che il DBMS mette a disposizione.
I livelli di isolamento previsti sono i seguenti:
- READ UNCOMMITED
- Con questo livello non si possono verificare solo
Lost Update
. Consente transazioni in sola lettura, senza bloccare la lettura dei dati. - READ COMMITTED
- Con questo livello di isolamento si possono verificare
Unrepeatable Read
ma nonDirty Read
. Con il suo utilizzo è previsto il rilascio immediato dei dati in lettura e il rilascio ritardato dei dati in scrittura. - REPEATABLE READ
- Con questo livello si possono verificare solo
Dirty Read
. In pratica rappresenta lo Strict 2PL. Utilizzandolo vengono bloccati sia i dati in lettura che in scrittura dei record coinvolti. - SERIALIZABLE
- Grazie a questo livello si raggiunge un isolamento perfetto evitando le anomalie viste.
Il livello SERIALIZABLE non risolve tutti i problemi: la sua dichiarazione richiede un numero elevato di blocchi e aumenta la competizione tra le transazioni per l'acquisizione di blocchi. Questo peggiora le prestazioni e aumenta drasticamente il rischio di stallo. La migliore soluzione sta nell'utilizzare un pò tutti i livelli di isolamento in base alle esigenze del dato o dell'applicazione.