Isolamento delle transazioni in App Engine

Max Ross

Secondo Wikipedia, il livello di isolamento di un sistema di gestione di database "definisce come/quando le modifiche apportate da un'operazione diventano visibili ad altre operazioni simultanee". L'obiettivo di questo articolo è spiegare l'isolamento di query e transazioni in Cloud Datastore utilizzato da App Engine. Dopo aver letto questo articolo, dovresti avere una migliore comprensione del comportamento di letture e scritture simultanee, sia all'interno che all'esterno delle transazioni.

All'interno delle transazioni: serializzabili

I quattro livelli di isolamento sono, dal più forte al più debole, Serializzabile, Lettura ripetibile, Lettura impegnata e Lettura non impegnata. Le transazioni Datastore soddisfano 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 serie, una dopo l'altra.

Per ulteriori informazioni, consulta la sezione Isolamento e coerenza nella documentazione relativa alle transazioni e l'articolo di Wikipedia sull'isolamento degli snapshot.

Transazioni esterne: impegno di lettura

Le operazioni del datastore al di fuori delle transazioni sono molto simili al livello di isolamento impegnato in lettura. Le entità recuperate dal datastore tramite query o recuperano vedono solo i dati impegnati. Un'entità recuperata non avrà mai dati con commit parziale (alcuni dei dati precedenti a un commit e altri di quelli successivi). Tuttavia, l'interazione tra query e transazioni è un po' più sottile e per capirla dobbiamo esaminare il processo di commit in modo più approfondito.

Procedura di commit

Quando un commit viene restituito correttamente, è garantito che venga applicata la transazione, ma ciò non significa che il risultato della scrittura sia immediatamente visibile ai lettori. L'applicazione di una transazione consiste in due obiettivi:

  • Traguardo A: il punto in cui sono state applicate le modifiche a un'entità
  • Traguardo B: il punto in cui sono state applicate le variazioni degli indici per tale entità

Mostra le frecce di avanzamento dalla transazione di commit alle modifiche delle entità visibili alle modifiche degli indici e delle entità visibili.

In Cloud Datastore, la transazione viene generalmente applicata completamente entro poche centinaia di millisecondi dalla restituzione del commit. Tuttavia, anche se non viene applicata completamente, le letture, le scritture e le query predecessori successive rifletteranno sempre i risultati del commit, poiché queste operazioni applicano qualsiasi modifica in sospeso prima dell'esecuzione. Tuttavia, le query che coprono 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 al momento successivo al traguardo A garantisce la versione più recente di quell'entità. Tuttavia, se una richiesta in parallelo esegue una query il cui predicato (la clausola WHERE, per i fan SQL/GQL) non è soddisfatto dall'entità pre-aggiornamento, ma è soddisfatto dall'entità post-aggiornamento, quest'ultima 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 periodi di tempo, è 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. È anche possibile che un set di risultati includa un'entità le cui proprietà, sempre in base al risultato di una ricerca per chiave, non soddisfano il predicato della query. Una query non può prendere in considerazione le transazioni comprese tra il punto saliente A e il punto saliente B quando decidi quali entità restituire. Verrà eseguita su dati inattivi, ma un'operazione get() sulle chiavi restituite otterrà sempre la versione più recente dell'entità. Ciò significa che potrebbero mancare risultati che corrispondono alla tua query oppure potresti ricevere risultati che non corrispondono una volta ottenuta l'entità corrispondente.

Esistono scenari in cui è garantita l'applicazione completa di eventuali modifiche in attesa prima dell'esecuzione della query, ad esempio qualsiasi query dei predecessori in Cloud Datastore. In questo caso, i risultati della query saranno sempre attuali e coerenti.

Esempi

Abbiamo fornito una spiegazione generale dell'interazione simultanea di aggiornamenti e query, ma se sei come me, in genere trovi più facile capire questi concetti attraverso esempi concreti. Vediamone alcuni. Inizieremo con alcuni semplici esempi, quindi terminaremo con quelli più interessanti.

Supponiamo di avere un'applicazione che archivia le 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 187 cm.

Abbiamo 2 entità Person nel datastore:

  • Adam, che è alto 182 centimetri.
  • Bob, che è alto 217 centimetri.

Esempio 1: aumentare l'altezza di Adam

Supponiamo che un'applicazione riceva due richieste sostanzialmente contemporaneamente. La prima richiesta aggiorna l'altezza di Adam da 68 pollici a 74 pollici. Uno scatto di crescita! La seconda richiesta chiama getTallPeople(). Cosa restituisce getTallPeople()?

La risposta dipende dalla relazione tra i due obiettivi di commit attivati dalla Richiesta 1 e la query getTallPeople() eseguita dalla Richiesta 2. Supponiamo che l'URL 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é non è stato ancora eseguito il commit dell'aggiornamento ad Adam che aumenta la sua altezza, pertanto la modifica non è ancora visibile per la query che emettiamo nella Richiesta 2.

Ora supponiamo che l'aspetto sia simile al seguente:

  • 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 l'obiettivo B, quindi gli aggiornamenti agli indici persone non sono ancora stati applicati. Di conseguenza, getTallPeople() restituisce solo Bob. Questo è un esempio di set di risultati che esclude un'entità le cui proprietà soddisfano il predicato di query.

Esempio 2: rendere Bob più corto (scusa, Mario)

In questo esempio la Richiesta 1 fa un'azione diversa. Invece di aumentare l'altezza di Adam da 178 a 210 cm, l'altezza di Roberto sarà ridotta da 188 a 180 cm. Ancora una volta, che cosa fa getTallPeople()

return?
  • 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 Mario. Perché? Poiché l'aggiornamento a Roberto che ne riduce l'altezza non è ancora stato eseguito il commit, quindi la modifica non è ancora visibile per la query che emettiamo nella Richiesta 2.

Ora supponiamo che l'aspetto sia simile al seguente:

  • Richiesta 1, put()
  • Richiesta 1, put()-->commit()
  • Richiesta 1, put()-->commit()-->traguardo A
  • Richiesta 1, put()-->commit()-->traguardo B
  • Richiesta 2, getTallPeople()

In questo scenario, getTallPeople() non restituirà nessuno. Perché? Perché l'aggiornamento a Roberto che ne riduce l'altezza è stato confermato quando eseguiamo la nostra query nella Richiesta 2.

Ora supponiamo che l'aspetto sia simile al seguente:

  • 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, quindi gli aggiornamenti degli indici di persone non sono ancora stati applicati. Di conseguenza, getTallPeople() restituisce comunque Bob, ma la proprietà altezza dell'entità Person che restituisce il valore aggiornato: 65. Questo è un esempio di un set 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 vicino al commit in lettura. Ovviamente esistono differenze significative, ma ora che conosci queste differenze e i motivi alla loro base, dovresti essere in una posizione migliore per prendere decisioni di progettazione intelligenti e correlate al datastore nelle tue applicazioni.