▦ Dati e database

Transazioni e proprietà ACID spiegate con un bonifico

Atomicità, coerenza, isolamento e durabilità: cosa garantisce una transazione, come usare BEGIN e COMMIT e quali errori evitare nel codice applicativo.

Un database non serve solo a conservare dati, ma a conservarli corretti anche quando qualcosa va storto: un programma che si blocca a metà, due utenti che modificano lo stesso record, un'interruzione di corrente. Lo strumento che rende possibile tutto questo è la transazione, e le garanzie che offre si riassumono nell'acronimo ACID.

L'esempio del bonifico

Il caso didattico per eccellenza è il trasferimento di denaro tra due conti. Per spostare 100 euro dal conto A al conto B servono due operazioni:

UPDATE conti SET saldo = saldo - 100 WHERE id = 'A';
UPDATE conti SET saldo = saldo + 100 WHERE id = 'B';

Se il sistema si interrompe tra la prima e la seconda istruzione, 100 euro spariscono nel nulla. Se le due istruzioni sono racchiuse in una transazione, questo non può succedere:

BEGIN;
UPDATE conti SET saldo = saldo - 100 WHERE id = 'A';
UPDATE conti SET saldo = saldo + 100 WHERE id = 'B';
COMMIT;

Fino al COMMIT le modifiche sono provvisorie. Se qualcosa fallisce, si esegue ROLLBACK e il database torna esattamente allo stato precedente.

A come atomicità

Una transazione è indivisibile: o tutte le sue operazioni hanno effetto, o nessuna. Non esiste uno stato intermedio visibile in cui il conto A è stato addebitato e il conto B non ancora accreditato. Se il server si spegne prima del commit, al riavvio la transazione risulta annullata.

C come coerenza

Una transazione porta il database da uno stato valido a un altro stato valido, rispettando tutti i vincoli definiti: chiavi primarie, chiavi esterne, CHECK, NOT NULL. Se per esempio la tabella ha il vincolo CHECK (saldo >= 0) e il conto A ha solo 50 euro, il primo UPDATE fallisce e l'intera transazione viene annullata.

È importante capire un limite: il database garantisce solo le regole che gli sono state dichiarate. Se la regola «il totale di tutti i saldi non cambia con un bonifico» esiste solo nella testa di chi ha scritto il programma, il database non può farla rispettare. Più regole si traducono in vincoli, più la coerenza diventa automatica.

I come isolamento

Molte transazioni girano nello stesso momento. L'isolamento stabilisce fino a che punto ciascuna vede le modifiche non ancora confermate delle altre. Idealmente ogni transazione dovrebbe comportarsi come se fosse l'unica in esecuzione, ma questo ha un costo in prestazioni. Per questo i database offrono diversi livelli di isolamento, che permettono di bilanciare correttezza e velocità. Un'altra transazione che legge i saldi durante il nostro bonifico, con i livelli più comuni, vede o lo stato prima o lo stato dopo, mai quello a metà.

D come durabilità

Una volta che il database ha risposto «commit eseguito», la modifica sopravvive anche a un crash immediatamente successivo. Per ottenerlo, i database scrivono le modifiche in un registro sequenziale, spesso chiamato write-ahead log o redo log, e lo forzano su disco prima di confermare. Al riavvio dopo un'interruzione, il registro viene usato per ricostruire le transazioni confermate e scartare quelle incomplete.

Alcune configurazioni permettono di rinunciare in parte a questa garanzia per guadagnare velocità, per esempio confermando prima che i dati siano davvero su disco. È una scelta legittima per dati ricostruibili, ma va fatta consapevolmente.

Transazioni nel codice applicativo

Nei linguaggi di programmazione le transazioni si gestiscono di solito con un blocco che fa commit se tutto va bene e rollback in caso di eccezione. In Python, con la libreria standard sqlite3:

import sqlite3

conn = sqlite3.connect("banca.db")
try:
    with conn:  # commit automatico se non ci sono eccezioni
        conn.execute("UPDATE conti SET saldo = saldo - ? WHERE id = ?", (100, "A"))
        conn.execute("UPDATE conti SET saldo = saldo + ? WHERE id = ?", (100, "B"))
except sqlite3.Error as errore:
    print("Bonifico annullato:", errore)

Il blocco with applicato alla connessione esegue il commit alla fine e il rollback se si verifica un'eccezione.

Gli errori più frequenti

Modalità autocommit dimenticata

Molti strumenti lavorano in autocommit: ogni istruzione è una transazione a sé. Se si eseguono due UPDATE di seguito senza BEGIN, non sono atomici insieme.

Transazioni troppo lunghe

Una transazione aperta mantiene blocchi sulle righe modificate e impedisce al database di fare pulizia delle versioni vecchie dei dati. Aprire una transazione, attendere una risposta da un servizio esterno o un'azione dell'utente e solo dopo chiuderla è un errore classico. Le transazioni devono essere brevi: si preparano i dati prima, si apre, si scrive, si chiude.

Azioni esterne dentro la transazione

Inviare un'email o chiamare un servizio di pagamento all'interno di una transazione crea un problema: se poi la transazione viene annullata, l'email è comunque partita. Le azioni esterne vanno eseguite dopo il commit, oppure registrate in una tabella di attività da svolgere che un processo separato elabora.

Errori ignorati

Se un'istruzione fallisce e il codice prosegue facendo comunque commit, si perdono le garanzie. In PostgreSQL una transazione in cui si è verificato un errore rifiuta ogni istruzione successiva fino al rollback, proprio per evitare questo scenario.

In sintesi

  • Una transazione raggruppa più operazioni che devono riuscire o fallire insieme.
  • Atomicità: tutto o niente. Coerenza: i vincoli dichiarati sono sempre rispettati.
  • Isolamento: le transazioni concorrenti non vedono stati intermedi. Durabilità: ciò che è confermato resta.
  • Nel codice, commit alla fine e rollback in caso di errore, con transazioni brevi.
  • Email e chiamate a servizi esterni vanno fatte dopo il commit.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica e in psicologia, unisce l'analisi tecnica alla comprensione del fattore umano.

Leggi anche