Questa pagina introduce diversi livelli di isolamento e spiega come funzionano in Spanner.
Il livello di isolamento è una proprietà del database che definisce quali dati sono visibili alle transazioni simultanee. Spanner supporta due dei livelli di isolamento definiti nello standard SQL ANSI/ISO: serializable e repeatable read. Quando crei una transazione, devi scegliere il livello di isolamento più appropriato per la transazione. Il livello di isolamento scelto consente alle singole transazioni di dare la priorità a vari fattori, come la latenza, il tasso di interruzione e se l'applicazione è sensibile agli effetti delle anomalie dei dati. La scelta migliore dipende dalle esigenze specifiche del carico di lavoro.
Isolamento serializzabile
L'isolamento serializzabile è il livello di isolamento predefinito in Spanner. In base all'isolamento serializzabile, Spanner ti offre le garanzie di controllo della concorrenza più rigorose per le transazioni, che si chiama coerenza esterna. Spanner si comporta come se tutte le transazioni fossero eseguite in sequenza, anche se in realtà le esegue su più server (e possibilmente in più data center) per ottenere prestazioni e disponibilità superiori rispetto ai database su un singolo server. Inoltre, se una transazione viene completata prima che un'altra inizi il commit, Spanner garantisce che i client vedano sempre i risultati delle transazioni in ordine sequenziale. Intuitivamente, Spanner è simile a un database di una singola macchina.
Il compromesso è che Spanner potrebbe interrompere le transazioni se un carico di lavoro ha un'elevata contesa di lettura/scrittura, in cui molte transazioni leggono dati che un'altra transazione sta aggiornando, a causa della natura fondamentale delle transazioni serializzabili. Tuttavia, si tratta di un buon valore predefinito per un database operativo. Ti aiuta a evitare problemi di sincronizzazione difficili che di solito si verificano solo con un'elevata concorrenza. Questi problemi sono difficili da riprodurre e risolvere. Pertanto, l'isolamento serializzabile offre la protezione più efficace contro le anomalie dei dati. Se è necessario riprovare una transazione, potrebbe verificarsi un aumento della latenza a causa dei nuovi tentativi.
Isolamento di lettura ripetibile
In Spanner, l'isolamento di lettura ripetibile viene implementato utilizzando una tecnica comunemente nota come isolamento degli snapshot. L'isolamento di lettura ripetibile in Spanner garantisce che tutte le operazioni di lettura all'interno di una transazione vedano uno snapshot coerente o forte del database così com'era all'inizio della transazione. Garantisce inoltre che le scritture simultanee sugli stessi dati vanno a buon fine solo se non ci sono conflitti. Questo approccio è vantaggioso in scenari di conflitto di lettura/scrittura elevato in cui numerose transazioni leggono dati che altre transazioni potrebbero modificare. Utilizzando uno snapshot fisso, la lettura ripetibile evita gli impatti sulle prestazioni del livello di isolamento serializzabile più restrittivo. Le letture possono essere eseguite senza acquisire blocchi e senza bloccare le scritture simultanee, il che comporta un minor numero di transazioni interrotte che potrebbero dover essere riprovate a causa di potenziali conflitti di serializzazione. Nei casi d'uso in cui i tuoi client eseguono già tutto in una transazione di lettura/scrittura ed è difficile riprogettare e utilizzare transazioni di sola lettura, puoi utilizzare l'isolamento di lettura ripetibile per migliorare la latenza dei tuoi carichi di lavoro.
A differenza dell'isolamento serializzabile, la lettura ripetibile potrebbe causare anomalie nei dati se l'applicazione si basa su relazioni o vincoli di dati specifici che non vengono applicati dallo schema del database, soprattutto quando l'ordine delle operazioni è importante. In questi casi, una transazione potrebbe leggere i dati, prendere decisioni in base a questi dati e quindi scrivere modifiche che violano i vincoli specifici dell'applicazione, anche se i vincoli dello schema del database sono ancora soddisfatti. Ciò accade perché l'isolamento di lettura ripetibile consente alle transazioni simultanee di procedere senza una serializzazione rigorosa. Una potenziale anomalia è nota come distorsione di scrittura, che deriva da un particolare tipo di aggiornamento simultaneo, in cui ogni aggiornamento viene accettato in modo indipendente, ma il loro effetto combinato viola l'integrità dei dati dell'applicazione. Ad esempio, immagina un sistema ospedaliero in cui almeno un medico deve essere reperibile in qualsiasi momento e i medici possono richiedere di non essere reperibili per un turno. In caso di isolamento di lettura ripetibile, se sia il dottor Richards che il dottor Smith sono programmati per essere di turno nello stesso turno e tentano contemporaneamente di richiedere di essere rimossi dal turno, ogni richiesta va a buon fine in parallelo. Questo perché entrambe le transazioni leggono che è previsto che almeno un altro medico sia di turno all'inizio della transazione, causando un'anomalia dei dati se le transazioni vanno a buon fine. D'altra parte, l'utilizzo dell'isolamento serializzabile impedisce a queste transazioni di violare il vincolo perché le transazioni serializzabili rileveranno potenziali anomalie dei dati e interromperanno la transazione. Garantendo così la coerenza dell'applicazione accettando tassi di interruzione più elevati.
Nell'esempio precedente, puoi utilizzare la clausola SELECT FOR UPDATE
nell'isolamento di lettura ripetibile.
La clausola SELECT…FOR UPDATE
verifica se i dati letti nello snapshot scelto rimangono invariati al momento del commit. Allo stesso modo, le istruzioni DML e le mutazioni, che leggono i dati internamente per garantire l'integrità delle scritture, verificano anche che i dati rimangano invariati al momento del commit.
Per saperne di più, consulta la sezione Utilizzare l'isolamento di lettura ripetibile.
Caso d'uso di esempio
L'esempio seguente mostra il vantaggio dell'utilizzo dell'isolamento di lettura ripetibile per eliminare l'overhead di blocco. Sia Transaction 1
che
Transaction 2
vengono eseguiti in isolamento di lettura ripetibile.
Transaction 1
stabilisce un timestamp dello snapshot quando viene eseguita l'istruzione SELECT
.
GoogleSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
/*-----------+------------------*
| AlbumId | MarketingBudget |
+------------+------------------+
| 1 | 50000 |
| 2 | 100000 |
| 3 | 70000 |
| 4 | 80000 |
*------------+------------------*/
PostgreSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
/*-----------+------------------*
| albumid | marketingbudget |
+------------+------------------+
| 1 | 50000 |
| 2 | 100000 |
| 3 | 70000 |
| 4 | 80000 |
*------------+------------------*/
Poi, Transaction 2
stabilisce un timestamp dello snapshot dopo l'inizio di Transaction 1
, ma prima del commit. Poiché Transaction 1
non ha aggiornato i dati, la
query SELECT
in Transaction 2
legge gli stessi dati di Transaction 1
.
GoogleSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 > T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);
COMMIT;
PostgreSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 > T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);
COMMIT;
Transaction 1
continua dopo che Transaction 2
ha eseguito il commit.
GoogleSQL
-- Transaction 1 continues
SELECT SUM(MarketingBudget) as UsedBudget
FROM Albums
WHERE SingerId = 1;
/*-----------*
| UsedBudget |
+------------+
| 300000 |
*------------*/
PostgreSQL
-- Transaction 1 continues
SELECT SUM(marketingbudget) AS usedbudget
FROM albums
WHERE singerid = 1;
/*-----------*
| usedbudget |
+------------+
| 300000 |
*------------*/
Il valore UsedBudget
restituito da Spanner è la somma del
budget letto da Transaction 1
. Questa somma riflette solo i dati presenti nello snapshot
T1
. Non include il budget aggiunto da Transaction 2
,
perché Transaction 2
si è impegnato dopo lo snapshot stabilito Transaction 1
T1
. L'utilizzo della lettura ripetibile significa che Transaction 1
non ha dovuto interrompere l'operazione anche se Transaction 2
ha modificato i dati letti da Transaction 1
. Tuttavia,
il risultato restituito da Spanner potrebbe o meno essere quello previsto.
Conflitti di lettura/scrittura e correttezza
Nell'esempio precedente, se i dati interrogati dalle istruzioni SELECT
in
Transaction 1
sono stati utilizzati per prendere decisioni successive sul budget di marketing, potrebbero
sorgere problemi di correttezza.
Ad esempio, supponiamo che il budget totale sia di 400,000
. In base al risultato
dell'estratto SELECT
in Transaction 1
, potremmo pensare che nel budget siano rimasti
100,000
e decidere di allocarli tutti a AlbumId = 4
.
GoogleSQL
-- Transaction 1 continues..
UPDATE Albums
SET MarketingBudget = MarketingBudget + 100000
WHERE SingerId = 1 AND AlbumId = 4;
COMMIT;
PostgreSQL
-- Transaction 1 continues..
UPDATE albums
SET marketingbudget = marketingbudget + 100000
WHERE singerid = 1 AND albumid = 4;
COMMIT;
Transaction 1
viene eseguito il commit correttamente, anche se Transaction 2
ha già
allocato 50,000
del budget rimanente di 100,000
a un nuovo album
AlbumId = 5
.
Puoi utilizzare la sintassi SELECT...FOR UPDATE
per verificare che determinate letture di una
transazione rimangano invariate durante la durata della transazione per
garantire la correttezza della transazione. Nell'esempio seguente che utilizza
SELECT...FOR UPDATE
, Transaction 1
viene interrotto al momento del commit.
GoogleSQL
-- Transaction 1 continues..
SELECT SUM(MarketingBudget) AS TotalBudget
FROM Albums
WHERE SingerId = 1
FOR UPDATE;
/*-----------*
| TotalBudget |
+------------+
| 300000 |
*------------*/
COMMIT;
PostgreSQL
-- Transaction 1 continues..
SELECT SUM(marketingbudget) AS totalbudget
FROM albums
WHERE singerid = 1
FOR UPDATE;
/*-------------*
| totalbudget |
+-------------+
| 300000 |
*-------------*/
COMMIT;
Per ulteriori informazioni, vedi Utilizzare SELECT FOR UPDATE nell'isolamento di lettura ripetibile.
Conflitti di scrittura-scrittura e correttezza
Utilizzando il livello di isolamento di lettura ripetibile, le scritture simultanee sugli stessi dati vanno a buon fine solo se non ci sono conflitti.
Nell'esempio seguente, Transaction 1
stabilisce un timestamp dello snapshot nella prima istruzione SELECT
.
GoogleSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
PostgreSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
Il seguente Transaction 2
legge gli stessi dati di Transaction 1
e inserisce
un nuovo elemento. Transaction 2
esegue il commit correttamente senza attendere o interrompere l'operazione.
GoogleSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 (> T1)
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);
COMMIT;
PostgreSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 (> T1)
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);
COMMIT;
Transaction 1
continua dopo che Transaction 2
ha eseguito il commit.
GoogleSQL
-- Transaction 1 continues
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;
PostgreSQL
-- Transaction 1 continues
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;
Transaction 1
interruzioni perché Transaction 2
ha già eseguito un inserimento
nella riga AlbumId = 5
.
Passaggi successivi
Scopri come utilizzare il livello di isolamento di lettura ripetibile.
Scopri come utilizzare SELECT FOR UPDATE nell'isolamento di lettura ripetibile.
Scopri come utilizzare SELECT FOR UPDATE nell'isolamento serializzabile.
Scopri di più sulla serializzabilità e sulla coerenza esterna di Spanner, consulta TrueTime e coerenza esterna.