MokaByte Numero 29  - Aprile 1999 
 
Le transazioni in Java
di 
Bagnoli Roberto
Gestione delle transazioni su database in Java


I dati memorizzati in un database (DB) sono soggetti a precisi vincoli di integrità, strettamente legati al contesto del problema cui i dati stessi si riferiscono.  Una condizione assolutamente irrinunciabile per un utilizzo corretto delle informazioni è che il DB si trovi sempre in uno stato consistente, vale a dire che esso soddisfi a tutti i vincoli per esso definiti, almeno dal punto di vista delle applicazioni che vi accedono.

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.

  1. la T1 aggiorna un dato X;
  2. la T2 aggiorna a sua volta lo stesso dato X;
  3. la T1 va in abort;
  4. il DBMS riporta il database allo stato precedente l’inizio di T1;
  5. 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)
  1. una transazione T1 legge un dato X;
  2. una seconda transazione T2 aggirona lo stesso dato X e va in commit;
  3. 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)
  1. una transazione T1 modifica un dato X
  2. una transazione T2 legge lo stesso dato X
  3. 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):
  1. chaos: non viene tentato alcunchè per prevenire le tre le anomalie citate;
  2. browse: si evitano solo i lost update;
  3. cursor stability: non si hanno nè lost update nè dirty read;
  4. 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:
  1. la transazione T1 richiede ed acquisisce il lock LR(D1) (lock in lettura sul dato D1);
  2. la transazione T2 richiede ed acquisisce il lock in lettura LR(D2);
  3. 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;
  4. 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.


 
 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it