Concorrenza nei database
Come abbiamo anticipato nella lezione sulle tecnologie di persistenza, gli RDBMS consentono di gestire più accessi in contemporanea allo stesso database, rendendoli ideali in scenari di utilizzo con più sessioni in contemporanea, tramite accesso parallelo.
Come noto, l’accesso in parallelo comporta però problematiche di concorrenza, quando due o più query eseguite in contemporanea leggono e scrivono in contemporanea. In questo senso si ricordano le condizioni di Bernstein:
R(Q1) ⋂ R(Q2) = ∅
R(Q1) ⋂ D(Q2) = ∅
D(Q1) ⋂ R(Q2) = ∅
dove Q1 e Q2 sono le due query eseguite in parallelo, R il rango (cioè l’insieme delle righe oggetto di scrittura) e D il dominio delle funzioni (cioè l’insieme delle righe oggetto di lettura). In pratica se due query eseguono una operazione sugli stessi dati (o parte di essi) ed anche solo una delle due è in scrittura, ci può essere un problema di concorrenza.
Concretamente si possono verificare tre scenari:
- Dirty Read: una query Q1 legge una tabella mentre Q2 sta facendo un inserimento: Q1 legge dati sporchi.
- Non Repeatable Read: la query Q1 legge una tabella mentre Q2 sta modificando i dati: Q1 legge dati vecchi.
- Phantom Read: Q1 legge righe che Q2 sta cancellando: le righe restituite da Q1 non esistono più.
Questo tipo di problemi sono tanto più complessi quanto più è complessa l’operazione parallela. Ipotizzando il progetto della libreria musicale, ipotizziamo di eseguire una query Q1 che vuole elencare tutti gli album rock che abbiamo: si tratta di una query con diverse JOIN (Album, Supporto, Genere).
SELECT ALBUM.Titolo, AUTORE.Nome, AUTORE.Cognome
FROM ALBUM
JOIN AUTORE ON ALBUM.AutoreId = AUTORE.Id
JOIN ALBUM_GENERE AG ON AG.AlbumId = ALBUM.Id
JOIN GENERE G ON AG.GenereId = G.Id
WHERE G.Descrizione = 'Rock';Contemporaneamente Q2 sta eliminando alcuni album:
DELETE FROM ALBUM
WHERE Titolo = 'Cosa succede in città';Come sappiamo, nel momento in cui eliminiamo un album, in cascata l’RDBMS elimina tutte le righe con la chiave esterna se non referenziate, quindi tutti i supporti utilizzati per quell’album. Le righe da tabele esterne sono eliminate PRIMA dell’eliminazione della riga dalla tabella principale.
Il rischio è che potremmo avere in questo caso è un Phantom Read, vedremo l’album ma in realtà sono stati cancellati tutti i supporti di quell’album. Situazioni analoghe (Dirty o Non Repeatable) possono accadere in caso di UPDATE o INSERT.
Occorre un meccanismo per garantire, come avviene nella programmazione concorrente, che ci sia una specie di sezione critica che consenta di gestire queste situazioni di inconsistenza dei dati.
Transazioni
Riscriviamo il codice della scrittura (cancellazione in questo modo):
BEGIN TRANSACTION;
DELETE FROM ALBUM
WHERE Titolo = 'Cosa succede in città';
COMMIT;Con l’istruzione BEGIN TRANSACTION stiamo dicendo al motore SQL (nello specifico Mysql) che tutte le operazioni seguenti fanno parte di una transazione in sezione critica, dove cioè non è ammesso parallelismo.
La transazione si può concludere in due modi:
COMMIT: tutte le operazioni indicate sono eseguite, bloccando tutte le tabelle fino ad operazione avvenuta: tutte le altre query di lettura (e scrittura) vengono sospese in attesa della conclusione della transazione;ROLLBACK: viene ripristinato lo stato precedente alla transazione, nessuna delle operazioni viene eseguita (vedi sotto).
Grazie a questo meccanismo una eventuale query di lettura eseguita in contemporanea (overlapping), ma prima del COMMIT, leggerà lo stato del database prima della transazione conclusa. Altrimenti si mette in attesa del COMMIT, e leggerà lo stato del database dopo la modifica di tutte le tabelle coinvolte.
Il sistema gestisce automaticamente ed internamente eventuali semafori e sezioni critiche, in modo trasparente per il programmatore. In altri termini, la transazione trasforma un insieme di query come se fossero una unica query eseguita come singola unità di lavoro in modalità mono utente.
Mysql nello specifico ha alcune limitazioni nelle transazioni: non è possibile eseguire query DDL dentro transazioni (sono eseguite senza COMMIT) e non possono essere eseguite transazioni annidate. Sono comunque casi del tutto speciali (i comandi DDL sono concettualmente indipendenti da operazioni sui dati, ed andrebbero eseguiti solo in caso di modifiche della struttura del database, in modalità monoutente). Le transazioni annidate sono utili per scomporre una transazione complessa in sotto transazioni (magari riutilizzate in diverse transazioni). Ad esempio si potrebbe ipotizzare di scomporre un inserimento complesso (album, autori, case discografiche, ecc.) in tante sottotransazioni indipendenti.
L’assenza di questo meccanismo in Mysql ne tradisce la natura un po’ meno “pro” rispetto a PostgreSQL, Oracle e SQL Server.
Rollback
Il rollback può essere invocato quando la query incontra una qualche difficoltà nella scrittura (dati inconsistenti).
BEGIN TRANSACTION;
DELETE FROM ALBUM
WHERE Titolo = 'Cosa succede in città';
-- succede qualche problema
ROLLBACK;L’RDBMS può comunque eseguire un rollback automatico in diversi scenari:
- tentativo di inserire una chiave primaria già esistente, o altri vincoli di integrità;
- deadlock: due transazioni hanno dipendenze circolari che le bloccano;
- timeout: una transazione attende un unlock da troppo tempo;
- errore a livello serializable: in caso di configurazione totalmente isolata (vedi sotto) il sistema fa rollback se c’è interferenza tra due transazioni.
Proprietà ACID
Le transazioni come detto hanno l’obiettivo di trasformare un insieme di query in una singola unità di lavoro, e devono rispettare quattro proprietà fondamentali, Atomicità, Consistenza, Isolamento e Durevolezza.
| Atomicità | La transazione è una unità di lavoro indivisibile. Le query sono eseguite o tutte (COMMIT) o nessuna (ROLLBACK). Non esiste alcun tipo di esecuzione parziale. |
| Consistenza | La transazione porta il database da uno stato valido ad un nuovo stato valido, rispettando quindi tutte le integrità referenziali e le CONSTRAINTS. |
| Isolamento | La transazione vede lo stato del database PRIMA del BEGIN, è cioè isolata da eventuali operazioni eseguite dopo. E’ il punto più complesso della transazione, che vedremo coi livelli di isolamento. |
| Durevolezza | Dopo una COMMIT non è possibile fare ROLLBACK. Di norma non solo i dati sono scritti sulle tabelle, ma esiste un log delle operazioni svolte, in caso di errori. |
Livelli di isolamento
Come detto l’isolamento è il tema tecnicamente più complesso: cosa succede se due transazioni in parallelo modificano le stesse tabelle? Vediamo i quattro scenari possibili:
| Tipo di isolamento | Descrizione | Protezione |
|---|---|---|
| Read Uncommited | Nessun isolamento: la transazione T1 legge le scritture di un’altra transazione T2 PRIMA del COMMIT da parte di T2. In pratica non c’è sezione critica, è molto veloce ma ovviamente può portare ad inconsistenze. Non protegge da Dirty Read. | Nessuna |
| Read Committed | Isolamento standard: T1 legge le scritture di T2 solo DOPO il COMMIT di T2, se questo è eseguito prima del COMMIT di T1. Questo garantisce di avere un database più aggiornato rispetto all’inizio della transazione. Protegge da Dirty Read ma non da Non Repeatable Read (due letture nella stessa transazione possono dare risultati diversi). | Dirty Read |
| Repeatable Read | Isolamento rinforzato: T1 legge lo stato del database ignorando qualsiasi altra commit eseguita nel frattempo da T2. Progette da Dirty Read e da Non Repeatable Read. Tuttavia non protegge da Phantom Read (righe cancellate). | Dirty Read Non Repeatable Read |
| Serializable | Isolamento totale. T1 viene eseguita interamente, e solo DOPO la COMMIT viene iniziata T2. E’ l’unico isolamento che soddisfa pienamente le condizioni di Bernstein. | Dirty Read Non Repeatable Read Phantom Read |
Secondo logica, si dovrebbe usare solo l’isolamento Serializable per garantire concorrenza perfetta. Di fatto però il costo prestazionale è tale da far preferire il rischio di qualche collisione.
Di default Mysql usa Repeatable Read, ma è possibile modificare il tipo di isolamento nella query:
SET SESSION ISOLATION LEVEL READ COMMITTED; -- oppure SERIALIZABLE o READ UNCOMMITTED;
BEGIN TRANSACTION;
...Conclusioni
In questa lezione abbiamo visto che siccome gli RDBMS sono progettati per gestire l’accesso in parallelo, prevedono dei meccanismi per proteggere scritture in concorrenza (vedi leggi di Bernstein), onde evitare problematiche come il Dirty Read, il Non Repeatable Read ed il Phantom Read.
Il meccanismo utilizzato è quello delle transazioni, che garantiscono le proprietà ACID: Atomicità, Consistenza, Isolamento, Durevolezza.
Merita particolare attenzione comprendere il livello di isolamento, che richiede un compromesso tra prestazioni (con rischio di collisioni) ed isolamento totale (più lento).
