Transazioni

Datastore supporta le transazioni. Una transazione è un'operazione o un insieme di operazioni atomiche: o si verificano tutte le operazioni nella transazione o non si verifica nessuna. Un'applicazione può eseguire più operazioni i calcoli in un'unica transazione.

Utilizzo delle transazioni

Una transazione è un insieme di operazioni di Datastore su una o più entità. Ogni transazione è garantita come atomica, il che significa che le transazioni non vengono mai applicate parzialmente. Qualsiasi operazione nel transazioni applicate o nessuna di queste viene applicata. 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:

  • Sono state 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 è obbligatorio utilizzare le transazioni per eseguire operazioni di Datastore.

La funzione datastore.RunInTransaction esegue la funzione fornita in una transazione.


package counter

import (
	"context"
	"fmt"
	"net/http"

	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/log"
	"google.golang.org/appengine/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 il commit della transazione, restituendo nil in caso di esito positivo. Se la funzione restituisce un valore non nil valore di errore, eventuali modifiche a Datastore non vengono applicate RunInTransaction restituisce lo stesso errore.

Se RunInTransaction non riesce a confermare la transazione a causa di un conflitto, lo riprova, rinunciando dopo tre tentativi. Ciò significa che la funzione di transazione deve essere idempotente, ovvero avere lo stesso risultato se eseguita più volte. Tieni presente che datastore.Get non è idempotente quando annullare il marshalling dei campi delle sezioni.

Operazioni consentite in una transazione

Datastore impone delle restrizioni su ciò che può essere fatto all'interno di una singola transazione.

Tutte le operazioni Datastore in una transazione devono operare nello stesso file gruppo di entità se si tratta di una transazione a un gruppo o su entità in un massimo di venticinque gruppi di entità se la transazione è tra gruppi. Questo include l'esecuzione di query sulle entità per predecessore, il recupero delle entità per chiave, l'aggiornamento entità e l'eliminazione di entità. Tieni presente che ogni entità principale appartiene a un gruppo di entità distinto, pertanto una singola transazione non può creare o operare su più di un'entità principale, a meno che non si tratti di una transazione tra gruppi.

Quando due o più transazioni tentano contemporaneamente di modificare entità in una o più gruppi di entità comuni, solo la prima transazione a cui viene eseguito il commit delle modifiche possono avere successo; tutti gli altri avranno esito negativo al momento del commit. A causa di questo design, l'utilizzo dei gruppi di entità limita il numero di scritture simultanee che puoi eseguire su qualsiasi entità dei gruppi. Quando inizia una transazione, Datastore utilizza il controllo della concorrenza ottimistico controllando l'ora dell'ultimo aggiornamento dei gruppi di entità utilizzati nella transazione. Al momento dell'commit di una transazione per i gruppi di entità, Datastore controlla di nuovo l'ora dell'ultimo aggiornamento dei gruppi di entità utilizzati nella transazione. Se è cambiato dal controllo iniziale, viene restituito un errore.

Isolamento e coerenza

Al di fuori delle transazioni, il livello di isolamento di Datastore è quello più vicino per leggere il commit. 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 i get all'interno di una transazione hanno la garanzia di vedere un singolo snapshot coerente di Datastore dall'inizio della transazione. Entità e le righe di indice nel gruppo di entità della transazione vengono aggiornate in modo che le query restituiscono l'insieme completo e corretto di entità di risultati, senza i falsi positivi o falsi negativi che possono verificarsi nelle query al di fuori delle transazioni.

Questa vista istantanea coerente si estende anche alle letture dopo le scritture all'interno delle transazioni. A differenza della maggior parte dei database, le query e i get all'interno di una transazione Datastore non vedono i risultati delle scritture precedenti all'interno della transazione. Nello specifico, se un'entità viene modificata o eliminata all'interno di una transazione, una query o un get restituisce la versione originale dell'entità dall'inizio della transazione o nessuna entità se l'entità non esisteva.

Utilizzi per le transazioni

Questo esempio mostra un utilizzo delle transazioni: l'aggiornamento di un'entità con un nuovo valore della proprietà rispetto al valore corrente.

func increment(ctx context.Context, key *datastore.Key) error {
	return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		count := new(Counter)
		if err := datastore.Get(ctx, key, count); err != nil {
			return err
		}
		count.Count++
		_, err := datastore.Put(ctx, key, count)
		return err
	}, nil)
}

Questa operazione richiede una transazione perché il valore potrebbe essere aggiornato da un altro utente dopo che questo codice recupera l'oggetto, ma prima di salvare l'oggetto modificato. Senza una transazione, la richiesta dell'utente utilizza il valore count precedente al valore l'aggiornamento di un 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 ripetuta fino al completamento di tutti i passaggi senza interruzioni.

Un altro utilizzo comune delle transazioni è recuperare un'entità con una chiave denominata o crearla se non esiste ancora:

type Account struct {
	Address string
	Phone   string
}

func GetOrUpdate(ctx context.Context, id, addr, phone string) error {
	key := datastore.NewKey(ctx, "Account", id, 0, nil)
	return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
		acct := new(Account)
		err := datastore.Get(ctx, key, acct)
		if err != nil && err != datastore.ErrNoSuchEntity {
			return err
		}
		acct.Address = addr
		acct.Phone = phone
		_, err = datastore.Put(ctx, key, acct)
		return err
	}, nil)
}

Come in precedenza, è necessaria una transazione per gestire il caso in cui un altro utente durante il tentativo di creare o aggiornare un'entità con lo stesso ID stringa. Senza un transazione, se l'entità non esiste e due utenti tentano di crearla, il secondo sostituisce la prima senza sapere che è successo.

Quando una transazione non va a buon fine, puoi chiedere all'app di riprovare la transazione finché non viene riesce oppure puoi consentire agli utenti di gestire l'errore propagandolo il livello dell'interfaccia utente dell'app. Non è necessario creare un ciclo di ripetizione per ogni transazione.

Infine, puoi utilizzare una transazione per leggere uno snapshot coerente Datastore. Questa opzione 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 a singolo gruppo di sola lettura non falliscono mai a causa di modifiche contemporaneamente, quindi non è necessario implementare i tentativi di nuovo in caso di errore. Tuttavia, le transazioni tra gruppi possono non riuscire a causa di modifiche contemporanee, pertanto dovrebbero avere dei nuovi tentativi. L'commit e il rollback di una transazione di sola lettura sono entrambi operazioni senza effetti.

Coda delle attività transazionali

Puoi mettere in coda un'attività nell'ambito di una transazione di Datastore, in modo che venga messa in coda solo (e sia garantito che venga messa in coda) se la transazione viene eseguita correttamente. Una volta inserita in coda, non è garantito che l'attività venga eseguita immediatamente, pertanto non è atomica con la transazione. Ancora, una volta in coda, l'attività verrà ripetuta fino a quando non riesce. Questo vale per qualsiasi attività accodato durante una funzione RunInTransaction.

Le attività transazionali sono utili perché ti consentono di combinare azioni non di Datastore con una transazione che dipendono dall'esito positivo della transazione (ad esempio l'invio di un'email per confermare un acquisto). Tu può anche collegare le azioni Datastore alla transazione, ad esempio esegui il commit delle modifiche ai gruppi di entità al di fuori della transazione solo se transazione completata correttamente.

Un'applicazione non può inserire più di cinque attività transazionali nelle code di attività durante una singola transazione. Le attività di transazione non devono avere nomi specificati dall'utente.

datastore.RunInTransaction(ctx, func(ctx context.Context) error {
	t := &taskqueue.Task{Path: "/path/to/worker"}
	if _, err := taskqueue.Add(ctx, t, ""); err != nil {
		return err
	}
	// ...
	return nil
}, nil)