Uno dei problemi più comuni che si verificano durante l'esecuzione di transazioni simultanee è il problema di lettura sporca. Una lettura sporca si verifica quando una transazione può leggere i dati che vengono modificati da un'altra transazione che è in esecuzione contemporaneamente ma che non ha ancora eseguito il commit.
Se la transazione che modifica i dati si impegna, il problema di lettura sporca non si verifica. Tuttavia, se la transazione che modifica i dati viene ripristinata dopo che l'altra transazione ha letto i dati, quest'ultima contiene dati sporchi che in realtà non esistono.
Come sempre, assicurati di aver eseguito correttamente il backup prima di sperimentare un nuovo codice. Se non sei sicuro, consulta questo articolo sul backup dei database MS SQL.
Capiamolo con l'aiuto di un esempio. Supponiamo di avere una tabella denominata "Prodotto" che memorizza ID, nome e ItemsinStock per il prodotto.
La tabella si presenta così:
[id tabella=20 /]
Si supponga di disporre di un sistema online in cui un utente può acquistare prodotti e visualizzare i prodotti allo stesso tempo. Dai un'occhiata alla figura seguente.
Considera uno scenario in cui un utente tenta di acquistare un prodotto. La transazione 1 eseguirà l'attività di acquisto per l'utente. Il primo passo nella transazione sarà aggiornare ItemsinStock.
Prima della transazione, ci sono 12 articoli in stock; la transazione lo aggiornerà a 11. La transazione ora comunicherà con un gateway di fatturazione esterno.
Se in questo momento, un'altra transazione, diciamo la Transazione 2, legge ItemsInStock per laptop, leggerà 11. Tuttavia, se successivamente l'utente dietro la Transazione 1 risulta avere fondi insufficienti nel suo account, la Transazione 1 verrà lanciata indietro e il valore della colonna ItemsInStock tornerà a 12.
Tuttavia, la transazione 2 ha 11 come valore per la colonna ItemsInStock. Questi sono dati sporchi e il problema è chiamato problema di lettura sporca.
Esempio funzionante di problema di lettura sporca
Diamo un'occhiata al problema di lettura sporca in azione in SQL Server. Come sempre, per prima cosa creiamo la nostra tabella e aggiungiamo alcuni dati fittizi ad essa. Esegui il seguente script sul tuo server di database.
CREATE DATABASE pos;
USE pos;
CREATE TABLE products
(
Id INT PRIMARY KEY,
Name VARCHAR(50) NOT NULL,
ItemsinStock INT NOT NULL
)
INSERT into products
VALUES
(1, 'Laptop', 12),
(2, 'iPhone', 15),
(3, 'Tablets', 10)
Ora apri due istanze di SQL Server Management Studio affiancate. Eseguiremo una transazione in ciascuna di queste istanze.
Aggiungi il seguente script alla prima istanza di SSMS.
USE pos;
SELECT * FROM products
-- Transaction 1
BEGIN Tran
UPDATE products set ItemsInStock = 11
WHERE Id = 1
-- Billing the customer
WaitFor Delay '00:00:10'
Rollback Transaction
Nello script precedente, avviamo una nuova transazione che aggiorna il valore per la colonna "ItemsInStock" della tabella prodotti in cui Id è 1. Simuliamo quindi il ritardo per la fatturazione al cliente utilizzando le funzioni "WaitFor" e "Delay". Nello script è stato impostato un ritardo di 10 secondi. Dopodiché, eseguiamo semplicemente il rollback della transazione.
Nella seconda istanza di SSMS, aggiungiamo semplicemente la seguente istruzione SELECT.
USE pos;
-- Transaction 2
SELECT * FROM products
WHERE Id = 1
Ora, esegui prima la prima transazione, ovvero esegui lo script nella prima istanza di SSMS, quindi esegui immediatamente lo script nella seconda istanza di SSMS.
Vedrai che entrambe le transazioni continueranno a essere eseguite per 10 secondi, dopodiché vedrai che il valore della colonna "ItemsInStock" per il record con Id 1 è ancora 12 come mostrato dalla seconda transazione. Sebbene la prima transazione l'abbia aggiornata a 11, abbia atteso 10 secondi e poi l'abbia ripristinata a 12, il valore mostrato dalla seconda transazione è 12 anziché 11.
Quello che è successo in realtà è che quando abbiamo eseguito la prima transazione, ha aggiornato il valore per la colonna "ItemsinStock". Ha quindi atteso 10 secondi e poi ha annullato la transazione.
Sebbene abbiamo avviato la seconda transazione subito dopo la prima, ha dovuto attendere il completamento della prima transazione. Ecco perché anche la seconda transazione ha atteso 10 secondi e perché la seconda transazione è stata eseguita immediatamente dopo che la prima transazione ha completato la sua esecuzione.
Leggi il livello di isolamento impegnato
Perché la transazione 2 ha dovuto attendere il completamento della transazione 1 prima di essere eseguita?
La risposta è che il livello di isolamento predefinito tra le transazioni è "read commit". Il livello di isolamento Read Committed garantisce che i dati possano essere letti da una transazione solo se si trova nello stato di commit.
Nel nostro esempio, la transazione 1 ha aggiornato i dati ma non li ha salvati fino a quando non è stato eseguito il rollback. Questo è il motivo per cui la transazione 2 ha dovuto attendere che la transazione 1 eseguisse il commit dei dati o il rollback della transazione prima di poter leggere i dati.
Ora, in scenari pratici, abbiamo spesso più transazioni che avvengono contemporaneamente su un singolo database e non vogliamo che ogni transazione debba aspettare il suo turno. Questo può rendere i database molto lenti. Immagina di acquistare qualcosa online da un grande sito web che potrebbe elaborare solo una transazione alla volta!
Lettura di dati non vincolati
La risposta a questo problema è consentire alle tue transazioni di funzionare con dati non vincolati.
Per leggere i dati non vincolati, è sufficiente impostare il livello di isolamento della transazione su "lettura non vincolata". Aggiorna la transazione 2 aggiungendo un livello di isolamento secondo lo script seguente.
USE pos;
-- Transaction 2
set transaction isolation level read uncommitted
SELECT * FROM products
WHERE Id = 1
Ora, se esegui la transazione 1 e quindi esegui immediatamente la transazione 2, vedrai che la transazione 2 non attenderà che la transazione 1 effettui il commit dei dati. La transazione 2 leggerà immediatamente i dati sporchi. Questo è mostrato nella figura seguente:
Qui l'istanza a sinistra esegue la transazione 1 e l'istanza a destra esegue la transazione 2.
Eseguiamo prima la transazione 1 che aggiorna il valore di "ItemsinStock" per l'id 1 a 11 da 12 e quindi attende 10 secondi prima di essere annullato.
Nel frattempo, la transazione w legge i dati sporchi che sono 11, come mostrato nella finestra dei risultati a destra. Poiché viene eseguito il rollback della transazione 1, questo non è il valore effettivo nella tabella. Il valore effettivo è 12. Prova a eseguire nuovamente la transazione 2 e vedrai che questa volta recupera 12.
La lettura senza commit è l'unico livello di isolamento che presenta il problema di lettura sporca. Questo livello di isolamento è il meno restrittivo di tutti i livelli di isolamento e consente di leggere i dati non vincolati.
Ovviamente, ci sono pro e contro nell'utilizzo di Read Uncommitted dipende dall'applicazione per cui viene utilizzato il database. Ovviamente, sarebbe una pessima idea utilizzarlo per il database dietro un sistema ATM e altri sistemi molto sicuri. Tuttavia, per le applicazioni in cui la velocità è molto importante (gestire grandi negozi di e-commerce) l'utilizzo di Read Uncommitted ha più senso.