Datastore supporta le transazioni. Una transazione è un'operazione o un insieme di operazioni atomiche: o si verificano tutte le operazioni nella transazione o nessuna. Un'applicazione può eseguire più operazioni e calcoli in una singola transazione.
Utilizzo delle transazioni
Una transazione è un insieme di operazioni Datastore su una o più entità. Ogni transazione è garantita come atomica, il che significa che le transazioni non vengono mai applicate parzialmente. Vengono applicate tutte le operazioni nella transazione o nessuna. Le transazioni hanno una durata massima di 60 secondi con un periodo di scadenza di inattività di 10 secondi dopo 30 secondi.
Un'operazione potrebbe non riuscire quando:
- Vengono tentate troppe modifiche simultanee sullo stesso gruppo di entità.
- La transazione supera un limite di risorse.
- Datastore rileva un errore interno.
In tutti questi casi, l'API Datastore restituisce un errore.
Le transazioni sono una funzionalità facoltativa di Datastore; non è necessario utilizzare le transazioni per eseguire operazioni Datastore.
La funzione datastore.RunInTransaction esegue la funzione fornita in una transazione.
package counter
import (
"context"
"fmt"
"net/http"
"google.golang.org/appengine/v2"
"google.golang.org/appengine/v2/datastore"
"google.golang.org/appengine/v2/log"
"google.golang.org/appengine/v2/taskqueue"
)
func init() {
http.HandleFunc("/", handler)
}
type Counter struct {
Count int
}
func handler(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)
key := datastore.NewKey(ctx, "Counter", "mycounter", 0, nil)
count := new(Counter)
err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
// Note: this function's argument ctx shadows the variable ctx
// from the surrounding function.
err := datastore.Get(ctx, key, count)
if err != nil && err != datastore.ErrNoSuchEntity {
return err
}
count.Count++
_, err = datastore.Put(ctx, key, count)
return err
}, nil)
if err != nil {
log.Errorf(ctx, "Transaction failed: %v", err)
http.Error(w, "Internal Server Error", 500)
return
}
fmt.Fprintf(w, "Current count: %d", count.Count)
}
Se la funzione restituisce nil, RunInTransaction tenta di eseguire la transazione e restituisce nil se l'operazione va a buon fine. Se la funzione restituisce un valore di errore non nil, le modifiche di Datastore non vengono applicate e RunInTransaction restituisce lo stesso errore.
Se RunInTransaction non riesce a eseguire la transazione a causa di un conflitto, riprova, rinunciando dopo tre tentativi. Ciò significa che la funzione di transazione deve essere idempotente, ovvero deve avere lo stesso risultato quando viene eseguita più volte. Tieni presente che datastore.Get non è idempotente quando si annulla la serializzazione dei campi di slice.
Cosa si può fare in una transazione
Datastore impone restrizioni su ciò che può essere fatto all'interno di una singola transazione.
Tutte le operazioni Datastore in una transazione devono operare su entità nello stesso gruppo di entità se la transazione è una transazione a gruppo singolo o su entità in un massimo di venticinque gruppi di entità se la transazione è una transazione tra gruppi. Ciò include l'esecuzione di query per le entità per predecessore, il recupero delle entità per chiave, l'aggiornamento delle entità e l'eliminazione delle entità. Tieni presente che ogni entità base appartiene a un gruppo di entità separato, quindi una singola transazione non può creare o operare su più di entità base a meno che non sia una transazione tra gruppi.
Quando due o più transazioni tentano contemporaneamente di modificare le entità in uno o più gruppi di entità comuni, solo la prima transazione che esegue le modifiche può avere esito positivo; tutte le altre non andranno a buon fine al momento dell'esecuzione. A causa di questa progettazione, l'utilizzo di gruppi di entità limita il numero di scritture simultanee che puoi eseguire su qualsiasi entità nei gruppi. Quando inizia una transazione, Datastore utilizza il controllo della concorrenza ottimistica controllando l'ultima ora di aggiornamento dei gruppi di entità utilizzati nella transazione. Al momento dell'esecuzione di una transazione per i gruppi di entità, Datastore controlla di nuovo l'ultima ora di aggiornamento dei gruppi di entità utilizzati nella transazione. Se è cambiato dal controllo iniziale,eviene restituito un errore.
Isolamento e coerenza
Al di fuori delle transazioni, il livello di isolamento di Datastore è più vicino a read committed. All'interno delle transazioni, viene applicato l'isolamento serializzabile. Ciò significa che un'altra transazione non può modificare contemporaneamente i dati letti o modificati da questa transazione.
In una transazione, tutte le letture riflettono lo stato attuale e coerente di Datastore al momento dell'inizio della transazione. Le query e le operazioni di recupero all'interno di una transazione hanno la garanzia di visualizzare un singolo snapshot coerente di Datastore all'inizio della transazione. Le entità e le righe di indice nel gruppo di entità della transazione vengono aggiornate completamente in modo che le query restituiscano l'insieme completo e corretto di entità di risultato, senza i falsi positivi o i falsi negativi che possono verificarsi nelle query al di fuori delle transazioni.
Questa visualizzazione di snapshot coerente si estende anche alle letture dopo le scritture all'interno delle transazioni. A differenza della maggior parte dei database, le query e le operazioni di recupero all'interno di una transazione Datastore non visualizzano i risultati delle scritture precedenti all'interno della transazione. In particolare, se un'entità viene modificata o eliminata all'interno di una transazione, una query o un'operazione di recupero restituisce la versione originale dell'entità all'inizio della transazione oppure non restituisce nulla se l'entità non esisteva.
Utilizzi delle transazioni
Questo esempio mostra un utilizzo delle transazioni: l'aggiornamento di un'entità con un nuovo valore di proprietà relativo al suo valore attuale.
Questa operazione richiede una transazione perché il valore potrebbe essere aggiornato da un altro utente dopo che questo codice recupera l'oggetto, ma prima che lo salvi.
Senza una transazione, la richiesta dell'utente utilizza il valore di count prima dell'aggiornamento dell'altro utente e il salvataggio sovrascrive il nuovo valore. Con una transazione, l'applicazione viene informata dell'aggiornamento dell'altro utente.
Se l'entità viene aggiornata durante la transazione, la transazione viene riprovata finché tutti i passaggi non vengono completati senza interruzioni.
Un altro utilizzo comune delle transazioni è recuperare un'entità con una chiave denominata o crearla se non esiste ancora:
Come in precedenza, è necessaria una transazione per gestire il caso in cui un altro utente tenti di creare o aggiornare un'entità con lo stesso ID stringa. Senza una transazione, se l'entità non esiste e due utenti tentano di crearla, la seconda sovrascrive la prima senza sapere che è successo.
Quando una transazione non va a buon fine, puoi fare in modo che la tua app riprovi la transazione finché non va a buon fine oppure puoi lasciare che gli utenti gestiscano l'errore propagandolo al livello dell'interfaccia utente dell'app. Non devi creare un loop di tentativi intorno a ogni transazione.
Infine, puoi utilizzare una transazione per leggere uno snapshot coerente di Datastore. Questo può essere utile quando sono necessarie più letture per eseguire il rendering di una pagina o esportare dati che devono essere coerenti. Questi tipi di transazioni sono spesso chiamate transazioni di sola lettura, poiché non eseguono scritture. Le transazioni di sola lettura a gruppo singolo non vanno mai a buon fine a causa di modifiche simultanee, quindi non devi implementare i tentativi in caso di errore. Tuttavia, le transazioni tra gruppi possono non andare a buon fine a causa di modifiche simultanee, quindi devono avere tentativi. L'esecuzione e l'annullamento di una transazione di sola lettura sono entrambe operazioni no-op.
Accodamento di attività transazionali
Puoi accodare un'attività come parte di una transazione Datastore, in modo che l'attività venga accodata (e sia garantita) solo se la transazione viene eseguita correttamente. Una volta accodata, l'attività non ha la garanzia di essere eseguita immediatamente, quindi non è atomica con la transazione. Tuttavia, una volta accodata, l'attività riproverà finché non va a buon fine. Questo vale per qualsiasi attività accodata durante una funzione RunInTransaction.
Le attività transazionali sono utili perché ti consentono di combinare azioni non Datastore a una transazione che dipende dal successo della transazione (ad esempio l'invio di un'email di conferma di un acquisto). Puoi anche collegare le azioni Datastore alla transazione, ad esempio per eseguire le modifiche ai gruppi di entità al di fuori della transazione se e solo se la transazione va a buon fine.
Un'applicazione non può inserire più di cinque attività transazionali nelle code di attività durante una singola transazione. Le attività transazionali non devono avere nomi specificati dall'utente.