Transazioni

Datastore supporta le transazioni. Una transazione è un'operazione o un insieme di operazioni atomico: tutte le operazioni nella transazione si verificano o non si verificano nessuna di queste. 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 per essere atomica, il che significa che le transazioni non vengono mai applicate parzialmente. Vengono applicate tutte le operazioni nella transazione o nessuna di queste viene applicata. Le transazioni hanno una durata massima di 60 secondi con una scadenza di inattività di 10 secondi dopo 30 secondi.

Un'operazione potrebbe non riuscire se:

  • Vengono tentate troppe modifiche simultanee allo stesso gruppo di entità.
  • La transazione supera il 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 le 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 se ha esito positivo. Se la funzione restituisce un valore diverso da nil errore, eventuali modifiche del datastore non vengono applicate e RunInTransaction restituisce lo stesso errore.

Se RunInTransaction non riesce a eseguire il commit della transazione a causa di un conflitto, riprova, rinunciando dopo tre tentativi. Ciò significa che la funzione transazione dovrebbe essere idempotente, il che significa che avrà lo stesso risultato se eseguita più volte. Tieni presente che datastore.Get non è idempotente quando esegui l'annullamento del marshalling dei campi sezione.

Che cosa può essere fatto in una transazione

Datastore impone limitazioni sulle operazioni che possono essere eseguite all'interno di una singola transazione.

Tutte le operazioni Datastore in una transazione devono operare su entità nello stesso gruppo di entità se si tratta di una transazione a gruppo singolo o su entità in un massimo di venticinque gruppi di entità se si tratta di una transazione tra gruppi. Ciò include query sulle entità per predecessore, recupero di entità per chiave, aggiornamento delle entità ed eliminazione di 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 un'entità base, a meno che non si tratti di 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 per eseguire il commit delle modifiche può andare a buon fine, mentre tutte le altre non andranno a buon fine con il commit. Grazie a questa struttura, l'utilizzo di gruppi di entità limita il numero di scritture simultanee che puoi eseguire su qualsiasi entità dei gruppi. Quando viene avviata una transazione, Datastore utilizza il controllo ottimistico della contemporaneità controllando l'ora dell'ultimo aggiornamento dei gruppi di entità utilizzati nella transazione. Dopo aver eseguito il commit di una transazione per i gruppi di entità, Datastore controlla di nuovo l'ora dell'ultimo aggiornamento per i 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 è il più vicino al commit di lettura. 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 nel momento in cui è iniziata la transazione. Per le query e l'accesso all'interno di una transazione è garantito un unico snapshot coerente del datastore a partire dall'inizio della transazione. Le entità e le righe dell'indice nel gruppo di entità della transazione vengono completamente aggiornate in modo che le query restituiscano l'insieme completo e corretto di entità di risultato, senza falsi positivi o falsi negativi che possono verificarsi nelle query al di fuori delle transazioni.

Questa visualizzazione coerente degli snapshot si estende anche alle letture dopo le scritture all'interno delle transazioni. A differenza della maggior parte dei database, le query e le interazioni all'interno di una transazione del datastore non mostrano 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à all'inizio della transazione o nulla 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 suo valore attuale.

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)
}

Ciò richiede una transazione perché il valore potrebbe essere aggiornato da un altro utente dopo che questo codice ha recuperato l'oggetto, ma prima che venga salvato l'oggetto modificato. 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, all'applicazione viene comunicato l'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, una transazione è necessaria 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, il secondo sovrascrive la prima senza sapere che è avvenuta.

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 consentire agli utenti di gestire l'errore propagandolo al livello di interfaccia utente della tua app. Non è necessario creare un ciclo di nuovo tentativo per 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 vengono spesso denominati transazioni di sola lettura, in quanto non eseguono operazioni di scrittura. Le transazioni per gruppo singolo di sola lettura non falliscono mai a causa di modifiche simultanee, quindi non è necessario implementare nuovi tentativi in caso di errore. Tuttavia, le transazioni tra gruppi possono avere esito negativo a causa di modifiche simultanee, pertanto dovrebbero essere effettuati nuovi tentativi. Il commit e il rollback di una transazione di sola lettura sono operazioni autonome.

Attività transazionale in coda

Puoi accodare un'attività come parte di una transazione Datastore, in modo che l'attività venga accodata solo (e la garanzia venga accodata) se il commit della transazione viene eseguito correttamente. Una volta accodato, non è garantito che l'attività venga eseguita immediatamente, quindi non è atomica con la transazione. Tuttavia, una volta inserita in coda, l'attività riproverà finché non va a buon fine. Questo si applica a qualsiasi attività in coda durante una funzione RunInTransaction.

Le attività transazionali sono utili perché consentono di combinare azioni non relative al datastore in una transazione in base alla riuscita della transazione (come l'invio di un'email per la conferma di un acquisto). Puoi anche associare le azioni Datastore alla transazione, ad esempio eseguire il commit delle modifiche ai gruppi di entità al di fuori della transazione solo se la transazione ha esito positivo.

Un'applicazione non può inserire più di cinque attività transazionali nelle code di attività che eseguono una singola transazione. Le attività transazionali 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)