Max Ross
Secondo Wikipedia, il livello di isolamento di un sistema di gestione del database "definisce come/quando le modifiche apportate da un'operazione diventano visibili ad altre operazioni concorrenti". Lo scopo di questo articolo è spiegare l'isolamento delle query e delle transazioni in Cloud Datastore utilizzato da App Engine. Dopo aver letto questo articolo, dovresti comprendere meglio il comportamento delle letture e delle scritture simultanee, sia all'interno che all'esterno delle transazioni.
Transazioni interne: serializzabile
In ordine dal più forte al più debole, i quattro livelli di isolamento sono Serializable, Lettura ripetibile, Lettura confermata e Lettura non confermata. Le transazioni Datastore il livello di isolamento Serializable. Ogni transazione è completamente isolata da tutte le altre transazioni e operazioni del datastore. Le transazioni su un determinato gruppo di entità vengono eseguite in sequenza, una dopo l'altra.
Per ulteriori informazioni, consulta la sezione Isolamento e coerenza della documentazione delle transazioni, nonché l'articolo di Wikipedia sull'isolamento degli snapshot.
Transazioni esterne: lettura confermata
Le operazioni di Datastore al di fuori delle transazioni sono molto simili al livello di isolamento Lettura confermata. Le entità recuperate dal datastore tramite query o get vedranno solo i dati committati. Un'entità recuperata non avrà mai dati sottoposti a commit parziale (alcuni prima di un commit e altri dopo). L'interazione tra query e transazioni è però un po' più complessa e, per comprenderla, dobbiamo esaminare più da vicino la procedura di commit.
La procedura di commit
Quando un commit viene restituito correttamente, la transazione viene applicata con garanzie, ma ciò non significa che il risultato della scrittura sia immediatamente visibile ai lettori. L'applicazione di una transazione prevede due traguardi:
- Obiettivo A: il punto in cui sono state applicate le modifiche a un'entità
- Obiettivo B: il punto in cui sono state applicate le modifiche agli indici per quell'entità
In Cloud Datastore, la transazione viene in genere applicata completamente entro alcune centinaia di millisecondi dal ritorno del commit. Tuttavia, anche se non viene applicato completamente, le letture, le scritture e le query sugli antenati successive rifletteranno sempre i risultati del commit, perché queste operazioni applicano eventuali modifiche in sospeso prima dell'esecuzione. Tuttavia, le query che interessano più gruppi di entità non possono determinare se sono presenti modifiche in sospeso prima dell'esecuzione e potrebbero restituire risultati obsoleti o applicati parzialmente.
Una richiesta che cerca un'entità aggiornata in base alla relativa chiave in un momento successivo al traguardo A garantisce di visualizzare la versione più recente dell'entità. Tuttavia, se una richiesta in parallelo esegue una query il cui predicato (la clausola WHERE
, per gli appassionati di SQL/GQL) non è soddisfatto dall'entità precedente all'aggiornamento, ma è soddisfatto dall'entità successiva all'aggiornamento, l'entità farà parte del set di risultati solo se la query viene eseguita dopo che l'operazione di applicazione ha raggiunto il traguardo B.
In altre parole, durante brevi finestre, è possibile che un insieme di risultati non includa un'entità le cui proprietà, in base al risultato di una ricerca per chiave, soddisfano il predicato della query. È inoltre possibile che un set di risultati includa un'entità le cui proprietà, sempre in base al risultato di una ricerca per chiave, non soddisfino il predicato della query. Una query non può prendere in considerazione le transazioni comprese tra il traguardo A e il traguardo B quando decide quali entità restituire. Verrà eseguita su dati non aggiornati, ma l'esecuzione di un'operazione get()
sulle chiavi restituite restituirà sempre la versione più recente dell'entità. Ciò significa che potresti non trovare risultati corrispondenti alla tua query o ottenere risultati che non corrispondono una volta ottenuta l'entità corrispondente.
Esistono scenari in cui è garantito che eventuali modifiche in attesa vengano applicate completamente prima dell'esecuzione della query, ad esempio qualsiasi query sull'antenato in Cloud Datastore. In questo caso, i risultati della query saranno sempre aggiornati e coerenti.
Esempi
Abbiamo fornito una spiegazione generale di come interagiscono gli aggiornamenti e le query simultanee, ma se sei come me, in genere è più facile comprendere questi concetti esaminando esempi concreti. Vediamone alcuni. Inizieremo con alcuni esempi semplici e poi concluderemo con quelli più interessanti.
Supponiamo di avere un'applicazione che memorizza entità Person. Una persona ha le seguenti proprietà:
- Nome
- Altezza
Questa applicazione supporta le seguenti operazioni:
updatePerson()
getTallPeople()
, che restituisce tutte le persone di altezza superiore a 180 cm.
Nel datastore sono presenti due entità Person:
- Adam, che è alto 173 cm.
- Bob, che è alto 185 cm.
Esempio 1: rendere Adam più alto
Supponiamo che un'applicazione riceva due richieste praticamente contemporaneamente. La prima richiesta aggiorna l'altezza di Adam da 173 cm a 188 cm. Un picco di crescita. La seconda richiesta chiama getTallPeople(). Che cosa restituisce getTallPeople()?
La risposta dipende dalla relazione tra i due traguardi di commit attivati dalla richiesta 1 e dalla query getTallPeople() eseguita dalla richiesta 2. Supponiamo che abbia il seguente aspetto:
- Richiesta 1,
put()
- Richiesta 2,
getTallPeople()
- Richiesta 1,
put()
-->commit()
- Richiesta 1,
put()
-->commit()
-->traguardo A - Richiesta 1,
put()
-->commit()
-->traguardo B
In questo scenario, getTallPeople()
restituirà solo Bob. Perché? Poiché l'update di Adam che aumenta la sua altezza non è stato ancora eseguito, la modifica non è ancora visibile alla query che emettiamo nella richiesta 2.
Supponiamo che abbia il seguente aspetto:
- Richiesta 1,
put()
- Richiesta 1,
put()
-->commit()
- Richiesta 1,
put()
-->commit()
-->traguardo A - Richiesta 2,
getTallPeople()
- Richiesta 1,
put()
-->commit()
-->traguardo B
In questo scenario, la query viene eseguita prima che la richiesta 1 raggiunga il traguardo B, pertanto gli aggiornamenti agli indici Person non sono ancora stati applicati. Di conseguenza, getTallPeople() restituisce solo Bob. Questo è un esempio di un insieme di risultati che esclude un'entità le cui proprietà soddisfano il predicato della query.
Esempio 2: accorciare Bob (scusa, Bob)
In questo esempio, la richiesta 1 eseguirà un'azione diversa. Invece di
aumentare l'altezza di Adam da 173 cm a 188 cm, ridurrà l'altezza di Bob da 188 cm a 165 cm. Ancora una volta, cosa fa getTallPeople()
- Richiesta 1,
put()
- Richiesta 2,
getTallPeople()
- Richiesta 1,
put()
-->commit()
- Richiesta 1,
put()
-->commit()
-->traguardo A - Richiesta 1,
put()
-->commit()
-->traguardo B
In questo scenario, getTallPeople()
restituirà solo Bob. Perché? Poiché
l'aggiornamento di Bob che riduce la sua altezza non è ancora stato eseguito,
la modifica non è ancora visibile alla query che emettiamo nella richiesta 2.
Supponiamo che abbia il seguente aspetto:
- Richiesta 1,
put()
- Richiesta 1,
put()
-->commit()
- Richiesta 1,
put()
-->commit()
-->traguardo A - Richiesta 1,
put()
-->commit()
-->traguardo B - Richiesta 2,
getTallPeople()
In questo caso, getTallPeople()
non restituirà nessuno. Perché? Perché l'
aggiornamento di Bob che riduce la sua altezza è stato eseguito al momento
in cui emettiamo la query nella richiesta 2.
Supponiamo che abbia il seguente aspetto:
- Richiesta 1,
put()
- Richiesta 1,
put()
-->commit()
- Richiesta 1,
put()
-->commit()
-->traguardo A - Richiesta 2,
getTallPeople()
- Richiesta 1,
put()
-->commit()
-->traguardo B
In questo scenario, la query viene eseguita prima del traguardo B, pertanto gli aggiornamenti agli indici Person non sono ancora stati applicati. Di conseguenza, getTallPeople()
restituisce ancora Bob, ma la proprietà altezza dell'entità Person restituita è il valore aggiornato: 65. Questo è un esempio di un insieme di risultati che include un'entità le cui proprietà non soddisfano il predicato della query.
Conclusione
Come puoi vedere dagli esempi precedenti, il livello di isolamento delle transazioni di Cloud Datastore è molto simile a Lettura confermata. Naturalmente, esistono differenze significative, ma ora che conosci queste differenze e i motivi alla base, dovresti essere in una posizione migliore per prendere decisioni di progettazione intelligenti e correlate al data store nelle tue applicazioni.