Questa pagina spiega le transazioni in Spanner e include codice campione per l'esecuzione delle transazioni.
Introduzione
Una transazione in Spanner è un insieme di letture e scritture eseguite a livello atomico in un singolo punto logico nel tempo su colonne, righe e tabelle di un database.
Spanner supporta queste modalità di transazione:
Blocco di lettura/scrittura. Queste transazioni si basano su blocchi pessimistici e, se necessario, su commit in due fasi. Il blocco delle transazioni di lettura/scrittura potrebbe interrompersi, richiedendo l'esecuzione dell'applicazione.
Sola lettura. Questo tipo di transazione offre coerenza garantita tra diverse letture, ma non consente scritture. Per impostazione predefinita, le transazioni di sola lettura vengono eseguite in corrispondenza di un timestamp scelto dal sistema che garantisce la coerenza esterna, ma possono anche essere configurate per la lettura in un timestamp precedente. Le transazioni di sola lettura non richiedono il commit e non prevedono blocchi. Inoltre, le transazioni di sola lettura potrebbero attendere il completamento delle scritture in corso prima dell'esecuzione.
DML partizionato. Questo tipo di transazione esegue un'istruzione DML (Data Manipulation Language) come DML partizionato. Il DML partizionato è progettato per gli aggiornamenti e le eliminazioni collettive, in particolare per operazioni di pulizia e backfill periodiche. Se devi eseguire il commit di un numero elevato di scritture cieche, ma non è necessaria una transazione atomica, puoi eseguire la modifica collettiva delle tabelle Spanner utilizzando la scrittura batch. Per maggiori informazioni, consulta la pagina Modificare i dati utilizzando le scritture in batch.
Questa pagina descrive le proprietà generali e la semantica delle transazioni in Spanner e introduce le interfacce delle transazioni DML partizionate, di lettura/scrittura e di sola lettura in Spanner.
Transazioni di lettura/scrittura
Ecco alcuni scenari in cui dovresti utilizzare una transazione di lettura e scrittura di blocco:
- Se esegui una scrittura che dipende dal risultato di una o più letture, dovresti farlo insieme alle operazioni di lettura nella stessa transazione di lettura/scrittura.
- Esempio: il doppio del saldo del conto bancario A. La lettura del saldo di A deve essere nella stessa transazione della scrittura per sostituire il saldo con il valore raddoppiato.
- Se esegui una o più scritture di cui deve essere eseguito il commit atomico, devi eseguirle nella stessa transazione di lettura/scrittura.
- Esempio: trasferisci 200 € dall'account A all'account B. Entrambe le operazioni di scrittura (una per ridurre A di 200 $e una per aumentare B di 200 $) e le letture dei saldi iniziali del conto devono essere nella stessa transazione.
- Se potresti eseguire una o più scritture, a seconda dei risultati di una o più letture, dovresti eseguirle nella stessa transazione di lettura/scrittura, anche se non vengono eseguite.
- Esempio: trasferisci 200 $dal conto bancario A al conto B se il saldo attuale di A è superiore a 500 $. La transazione deve contenere una lettura del saldo di A e un estratto conto condizionale contenente le scritture.
Ecco uno scenario in cui non devi utilizzare una transazione di lettura e scrittura di blocco:
- Se esegui solo operazioni di lettura e puoi esprimere la lettura utilizzando un metodo di lettura singola, devi utilizzare quel metodo di lettura singolo o una transazione di sola lettura. A differenza delle transazioni di lettura/scrittura, le letture singole non bloccano.
Proprietà
Una transazione di lettura/scrittura in Spanner esegue un insieme di letture e scritture a livello atomico in un singolo punto logico nel tempo. Inoltre, il timestamp in cui vengono eseguite le transazioni di lettura-scrittura corrisponde alle ore effettive, mentre l'ordine di serializzazione corrisponde all'ordine del timestamp.
Perché utilizzare una transazione di lettura/scrittura? Le transazioni di lettura-scrittura offrono le proprietà ACID dei database relazionali (in effetti, le transazioni di lettura-scrittura di Spanner offrono garanzie ancora maggiori rispetto ad ACID tradizionale; consulta la sezione Semantica di seguito).
Isolamento
Di seguito sono riportate le proprietà di isolamento per le transazioni di lettura-scrittura e di sola lettura.
Transazioni di lettura e scrittura
Di seguito sono riportate le proprietà di isolamento che ottieni dopo aver eseguito correttamente il commit di una transazione contenente una serie di letture (o query) e scritture:
- Tutte le letture all'interno della transazione hanno restituito valori che riflettono uno snapshot coerente registrato al timestamp di commit della transazione.
- Le righe o gli intervalli vuoti sono rimasti così al momento del commit.
- Tutte le scritture all'interno della transazione sono state impegnate al timestamp del commit della transazione.
- Le scritture non erano visibili in nessuna transazione fino all'invio della transazione.
Alcuni driver client Spanner contengono logica per i nuovi tentativi delle transazioni per mascherare gli errori temporanei, eseguendo di nuovo la transazione e convalidando i dati osservati dal client.
Il risultato è che tutte le operazioni di lettura e scrittura sembrano essere avvenute in un unico momento, sia dal punto di vista della transazione stessa che da quello di altri lettori e writer fino al database Spanner. In altre parole, le letture e le scritture finiscono per verificarsi nello stesso timestamp (vedi un'illustrazione nella sezione Serializzabilità e coerenza esterna di seguito).
Transazioni di sola lettura
Le garanzie per una transazione di lettura-scrittura che si limitano a operazioni di lettura sono simili: tutte le letture all'interno di quella transazione restituiscono dati dallo stesso timestamp, anche per la mancata esistenza di una riga. Una differenza è che se leggi i dati e successivamente esegui il commit della transazione di lettura/scrittura senza alcuna scrittura, non vi è alcuna garanzia che i dati non siano cambiati nel database dopo la lettura e prima del commit. Se vuoi sapere se i dati sono cambiati dall'ultima lettura, l'approccio migliore è rileggerli (in una transazione di lettura/scrittura o utilizzando una lettura efficace). Inoltre, per maggiore efficienza, se sai in anticipo che leggerai e non scriverai, dovresti utilizzare una transazione di sola lettura anziché una transazione di lettura/scrittura.
Atomicità, coerenza, durabilità
Oltre alla proprietà Isolation, Spanner offre Atomicità (se una qualsiasi delle scritture nel commit della transazione viene eseguita tutte), Coerenza (il database rimane in uno stato coerente dopo la transazione) e Durabilità (il commit dei dati impegnati viene mantenuto).
Vantaggi di queste strutture
Grazie a queste proprietà, in qualità di sviluppatore di applicazioni puoi concentrarti sulla correttezza di ogni transazione autonomamente, senza preoccuparti di come proteggerne l'esecuzione da altre transazioni che potrebbero essere eseguite contemporaneamente.
Interfaccia
Le librerie client di Spanner offrono un'interfaccia per l'esecuzione di un lavoro nel contesto di una transazione di lettura-scrittura, con nuovi tentativi di interruzione delle transazioni. Ecco un po' di contesto per spiegare questo punto: potrebbe essere necessario provare una transazione Spanner più volte prima del commit. Ad esempio, se due transazioni tentano di lavorare sui dati contemporaneamente in un modo che potrebbe causare un deadlock, Spanner ne interrompe una in modo che l'altra transazione possa progredire. Più raramente, gli eventi temporanei all'interno di Spanner possono comportare l'interruzione di alcune transazioni. Poiché le transazioni sono atomiche, una transazione interrotta non ha alcun effetto visibile sul database. Di conseguenza, le transazioni devono essere eseguite riprovando fino a quando non riescono.
Quando utilizzi una transazione in una libreria client Spanner, definisci il corpo di una transazione (ovvero le operazioni di lettura e scrittura da eseguire su una o più tabelle in un database) sotto forma di oggetto funzione. In background, la libreria client di Spanner esegue la funzione ripetutamente fino a quando non viene eseguito il commit della transazione o non si verifica un errore non ripetibile.
Esempio
Supponi di aver aggiunto una colonna MarketingBudget
alla
tabella Albums
mostrata nella
pagina Schema e modello dei dati:
CREATE TABLE Albums ( SingerId INT64 NOT NULL, AlbumId INT64 NOT NULL, AlbumTitle STRING(MAX), MarketingBudget INT64 ) PRIMARY KEY (SingerId, AlbumId);
Il reparto marketing decide di intraprendere una spinta di marketing per l'album relativo a
Albums (1, 1)
e ti ha chiesto di trasferire 200.000 $dal budget di Albums
(2, 2)
, ma solo se il denaro è disponibile nel budget dell'album. Dovresti
utilizzare una transazione di blocco di lettura-scrittura per questa operazione, poiché la transazione potrebbe eseguire scritture a seconda del risultato di una lettura.
Di seguito viene illustrato come eseguire una transazione di lettura/scrittura:
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
Semantica
Serializzabilità e coerenza esterna
Spanner offre la "serializzabilità", il che significa che tutte le transazioni sembrano eseguite in un ordine seriale, anche se alcune operazioni di lettura, scrittura e altre operazioni di transazioni distinte si sono effettivamente verificate in parallelo. Spanner assegna timestamp di commit che riflettono l'ordine delle transazioni impegnate per implementare questa proprietà. Infatti, Spanner offre una garanzia più efficace rispetto alla serializzabilità chiamata coerenza esterna: le transazioni vengono eseguite in un ordine che si riflette nei timestamp di commit e questi timestamp riflettono in tempo reale, quindi puoi confrontarli con il tuo smartwatch. Le letture in una transazione vedono tutto ciò che è stato eseguito prima del commit della transazione, mentre le scritture sono visualizzate da tutto ciò che inizia dopo il commit della transazione.
Ad esempio, considera l'esecuzione di due transazioni come illustrato nel seguente diagramma:
La transazione Txn1
in blu legge alcuni dati A
, memorizza nel buffer una scrittura in A
e poi
viene eseguito il commit. La transazione Txn2
in verde inizia dopo il giorno Txn1
, legge
alcuni dati il giorno B
, poi legge i dati A
. Poiché Txn2
legge il valore di A
dopo che Txn1
ha eseguito il commit della sua scrittura in A
, Txn2
vede l'effetto della scrittura di Txn1
in A
, anche se Txn2
è iniziato prima del completamento di Txn1
.
Anche se si verifica una sovrapposizione nel tempo in cui vengono eseguiti entrambi Txn1
e Txn2
, i rispettivi timestamp di commit c1
e c2
rispettano un ordine lineare di transazione, il che significa che tutti gli effetti delle letture e delle scritture di Txn1
sembrano essersi verificati in un unico momento (c1
) e tutti gli effetti delle letture e delle scritture di Txn2
sembrano essersi verificati in un unico momento in entrambe le macchine (c2
), anche se questo è garantito in modo diverso se Txn1
e le scritture sono state eseguite in un unico momento (c2
).Txn1
Txn2
Txn2
c1 < c2
(Tuttavia, se Txn2
ha letto solo nella transazione, allora c1 <= c2
).
Le letture osservano un prefisso della cronologia di commit; se una lettura vede l'effetto
Txn2
, vede anche l'effetto di Txn1
. Tutte le transazioni eseguite correttamente hanno questa proprietà.
Garanzie di lettura e scrittura
Se una chiamata per eseguire una transazione non va a buon fine, le garanzie di lettura e scrittura dipendono dall'errore con cui la chiamata di commit sottostante non è andata a buon fine.
Ad esempio, un errore come "Riga non trovata" o "Riga già esistente" significa che si è verificato un errore durante la scrittura delle mutazioni memorizzate nel buffer. Ad esempio, una riga che il client sta tentando di aggiornare non esiste. In questo caso, le letture sono garantite coerenti, le scritture non vengono applicate e la non esistenza della riga è garantita in modo coerente con le letture.
Annullamento delle operazioni di transazione in corso
Le operazioni di lettura asincrona possono essere annullate in qualsiasi momento dall'utente (ad esempio, quando viene annullata un'operazione di livello superiore o se decidi di interrompere una lettura in base ai risultati iniziali ricevuti dalla lettura) senza influire su altre operazioni esistenti nella transazione.
Tuttavia, anche se hai tentato di annullare la lettura, Spanner non garantisce che la lettura venga effettivamente annullata. Dopo aver richiesto l'annullamento di una lettura, questa lettura può comunque essere completata correttamente o non riuscire per qualche altro motivo (ad esempio, Interrompi). Inoltre, la lettura annullata potrebbe in realtà restituire alcuni risultati e quei risultati potenzialmente incompleti verranno convalidati come parte del commit della transazione.
Tieni presente che, a differenza delle letture, l'annullamento di un'operazione di commit di transazione comporterà l'interruzione della transazione (a meno che la transazione non sia già stata impegnata o non sia andata a buon fine per un altro motivo).
Prestazioni
Chiusura in corso
Spanner consente a più client di interagire contemporaneamente con lo stesso database. Per garantire la coerenza di più transazioni simultanee, Spanner utilizza una combinazione di blocchi condivisi e blocchi esclusivi per controllare l'accesso ai dati. Quando esegui una lettura come parte di una transazione, Spanner acquisisce blocchi di lettura condivisi, che consentono agli altri lettori di accedere comunque ai dati fino a quando la transazione non è pronta per il commit. Quando viene eseguito il commit della transazione e vengono applicate le scritture, la transazione tenta di eseguire l'upgrade a un blocco esclusivo. Blocca i nuovi blocchi di lettura condivisi sui dati, attende la cancellazione dei blocchi di lettura condivisi esistenti, quindi applica un blocco esclusivo per l'accesso esclusivo ai dati.
Note sulle serrature:
- I blocchi vengono applicati al livello di granularità di righe e colonne. Se la transazione T1 ha bloccato la colonna "A" della riga "foo" e la transazione T2 vuole scrivere la colonna "B" della riga "foo", non ci sono conflitti.
- Scrive in un elemento dati che non legge anche i dati scritti (ovvero "scritture cieche") non sono in conflitto con altri blind Writer dello stesso elemento (il timestamp di commit di ogni scrittura determina l'ordine in cui viene applicata al database). Di conseguenza, Spanner deve eseguire l'upgrade a un blocco esclusivo solo se hai letto i dati che scrivi. In caso contrario, Spanner utilizza un blocco condiviso chiamato blocco condiviso dello scrittore.
- Quando esegui ricerche di righe all'interno di una transazione di lettura-scrittura, utilizza indici secondari per limitare le righe scansionate a un intervallo più piccolo. In questo modo, Spanner blocca un numero inferiore di righe nella tabella, consentendo la modifica simultanea di righe al di fuori dell'intervallo.
Non utilizzare i blocchi per garantire l'accesso esclusivo a una risorsa esterna a Spanner. Le transazioni possono essere interrotte per diversi motivi da Spanner, ad esempio quando si consente lo spostamento dei dati all'interno delle risorse di calcolo dell'istanza. Se si tenta di nuovo una transazione, esplicitamente dal codice dell'applicazione o implicitamente dal codice client come il driver JDBC di Spanner, viene garantito solo che i blocchi siano stati trattenuti durante il tentativo effettivo.
Puoi utilizzare lo strumento introduttivo Statistiche sul blocco per esaminare i conflitti di blocco nel database.
Rilevamento deadlock
Spanner rileva quando più transazioni potrebbero essere bloccate e forza l'interruzione di tutte le transazioni tranne una. Ad esempio, considera lo
scenario seguente: la transazione Txn1
contiene un blocco nel record A
, è in attesa
di un blocco nel record B
, mentre Txn2
mantiene un blocco nel record B
ed è in attesa
di un blocco nel record A
. L'unico modo per fare progressi in questa situazione è interrompere una delle transazioni in modo da rilasciare il relativo blocco e consentire all'altra transazione di procedere.
Spanner usa l'algoritmo standard "wound-wait" per gestire il rilevamento del deadlock. Spanner tiene traccia dell'età di ogni transazione che richiede blocchi in conflitto. Inoltre, consente alle transazioni meno recenti di interrompere quelle più giovani (dove "meno recenti" significa che i dati, le query o il commit della transazione più vecchi si sono verificati prima).
Dando la priorità alle transazioni precedenti, Spanner garantisce che ogni transazione abbia la possibilità di acquisire blocchi alla fine, quando diventa abbastanza vecchio da avere una priorità più alta rispetto alle altre transazioni. Ad esempio, una transazione che acquisisce un blocco condiviso del lettore può essere interrotta da una transazione precedente che richiede un blocco condiviso dello scrittore.
Esecuzione distribuita
Spanner può eseguire transazioni su dati su più server. Questo vantaggio ha un costo in termini di prestazioni rispetto alle transazioni singolo server.
Quali tipi di transazioni potrebbero essere distribuiti? Di base, Spanner può suddividere la responsabilità delle righe del database tra più server. In genere, una riga e le righe corrispondenti nelle tabelle con interleaving sono gestite dallo stesso server, così come due righe della stessa tabella con chiavi vicine. Spanner può eseguire transazioni su righe diverse su server diversi. Tuttavia, come regola generale, le transazioni che interessano molte righe con posizioni condivise sono più veloci ed economiche rispetto alle transazioni che interessano molte righe sparse nel database o in una tabella di grandi dimensioni.
Le transazioni più efficienti in Spanner includono solo le letture e le scritture che devono essere applicate a livello atomico. Le transazioni sono più veloci quando tutte le leggono e scrivono i dati di accesso nella stessa parte dello spazio delle chiavi.
Transazioni di sola lettura
Oltre a bloccare le transazioni di lettura-scrittura, Spanner offre transazioni di sola lettura.
Utilizza una transazione di sola lettura quando devi eseguire più di una lettura allo stesso timestamp. Se puoi esprimere la lettura utilizzando uno dei metodi di lettura singola di Spanner, devi utilizzare invece quel metodo di lettura singolo. Le prestazioni dell'utilizzo di una singola chiamata di lettura devono essere paragonabili a quelle di una singola lettura eseguita in una transazione di sola lettura.
Se leggi una grande quantità di dati, valuta la possibilità di utilizzare le partizioni per leggere i dati in parallelo.
Poiché le transazioni di sola lettura non scrivono, non impostano blocchi e non bloccano altre transazioni. Le transazioni di sola lettura osservano un prefisso coerente della cronologia di commit delle transazioni, in modo che la tua applicazione riceva sempre dati coerenti.
Proprietà
Una transazione Spanner di sola lettura esegue un set di letture in un singolo momento logico, sia dal punto di vista della transazione di sola lettura stessa che dal punto di vista di altri lettori e writer fino al database Spanner. Ciò significa che le transazioni di sola lettura osservano sempre uno stato coerente del database in un punto selezionato della cronologia delle transazioni.
Interfaccia
Spanner offre un'interfaccia per l'esecuzione di un corpo di lavoro nel contesto di una transazione di sola lettura, con nuovi tentativi per le transazioni interrotte.
Esempio
Di seguito viene illustrato come utilizzare una transazione di sola lettura per ottenere dati coerenti per due letture allo stesso timestamp:
C++
C#
Go
Java
Node.js
PHP
Python
Ruby
Transazioni DML partizionate
Con il Partitioned Data Manipulation Language (DML partizionato), puoi eseguire istruzioni UPDATE
e DELETE
su larga scala senza imporre limiti alle transazioni o bloccare un'intera tabella.
Spanner esegue il partizionamento dello spazio delle chiavi ed esegue le istruzioni DML su ogni partizione in una transazione di lettura/scrittura separata.
Esegui istruzioni DML nelle transazioni di lettura/scrittura che crei esplicitamente nel codice. Per maggiori informazioni, consulta la pagina Utilizzo di DML.
Proprietà
Puoi eseguire una sola istruzione DML partizionata alla volta, sia che utilizzi un metodo di libreria client o Google Cloud CLI.
Le transazioni partizionate non supportano il commit o il rollback. Spanner esegue e applica immediatamente l'istruzione DML. Se annulli l'operazione o se l'operazione non va a buon fine, Spanner annulla tutte le partizioni in esecuzione e non avvia nessuna delle partizioni rimanenti. Spanner non esegue il rollback di nessuna partizione già eseguita.
Interfaccia
Spanner fornisce un'interfaccia per l'esecuzione di una singola istruzione DML partizionata.
Esempi
Il seguente esempio di codice aggiorna la colonna MarketingBudget
della tabella Albums
.
C++
Puoi usare la funzione ExecutePartitionedDml()
per eseguire un'istruzione DML partizionata.
C#
Utilizzerai il metodo ExecutePartitionedUpdateAsync()
per eseguire un'istruzione DML partizionata.
Go
Utilizzerai il metodo PartitionedUpdate()
per eseguire un'istruzione DML partizionata.
Java
Utilizzerai il metodo executePartitionedUpdate()
per eseguire un'istruzione DML partizionata.
Node.js
Utilizzerai il metodo runPartitionedUpdate()
per eseguire un'istruzione DML partizionata.
PHP
Utilizzerai il metodo executePartitionedUpdate()
per eseguire un'istruzione DML partizionata.
Python
Utilizzerai il metodo execute_partitioned_dml()
per eseguire un'istruzione DML partizionata.
Ruby
Utilizzerai il metodo execute_partitioned_update()
per eseguire un'istruzione DML partizionata.
Il seguente esempio di codice elimina le righe dalla tabella Singers
in base alla colonna SingerId
.