Transaktionen

Datastore unterstützt Transaktionen. Eine Transaktion ist ein unteilbarer Vorgang oder eine Reihe von unteilbaren Vorgängen, d. h., entweder werden alle Vorgänge in der Transaktion oder es wird keiner von ihnen ausgeführt. Eine Anwendung kann mehrere Vorgänge und Berechnungen in einer einzigen Transaktion durchführen.

Transaktionen verwenden

Eine Transaktion besteht aus einer Reihe von Datastore-Vorgängen, die auf eine oder mehrere Entitäten angewendet werden. Jede Transaktion ist garantiert unteilbar. Transaktionen werden also niemals nur teilweise angewendet. Entweder werden alle Vorgänge in der Transaktion angewendet oder keiner. Transaktionen haben eine maximale Dauer von 60 Sekunden mit einer Inaktivitätsablaufzeit von 10 Sekunden nach 30 Sekunden.

Ein Vorgang schlägt möglicherweise in folgenden Fällen fehl:

  • Die Anzahl der gleichzeitigen Änderungen für eine Entitätengruppe ist zu hoch.
  • Die Transaktion überschreitet einen Ressourcengrenzwert.
  • Datastore hat einen internen Fehler festgestellt.

In allen diesen Fällen gibt die Datastore API einen Fehler zurück.

Transaktionen sind ein optionales Feature von Datastore. Für Datastore-Vorgänge sind Transaktionen nicht erforderlich.

Die Funktion datastore.RunInTransaction führt die angegebene Funktion in einer Transaktion aus.


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

Wenn die Funktion nil zurückgibt, versucht RunInTransaction die Transaktion in einem Commit-Vorgang zu übergeben, wobei bei Erfolg nil zurückgegeben wird. Wenn die Funktion einen anderen Fehlerwert als nil zurückgibt, werden etwaige Datenspeicheränderungen nicht übernommen und RunInTransaction gibt denselben Fehler zurück.

Wenn RunInTransaction aufgrund eines Konflikts kein Commit der Transaktion durchführen kann, wird der Versuch wiederholt, wobei nach drei Versuchen aufgegeben wird. Dies bedeutet, dass die Transaktionsfunktion idempotent sein muss, d. h., die Versuche müssen bei einer mehrfachen Ausführung das gleiche Ergebnis haben. Beachten Sie, dass datastore.Get nicht idempotent ist, wenn ein Unmarshalling von Segmentfeldern ausgeführt wird.

Möglichkeiten in einer Transaktion

Die Funktionen innerhalb einer einzelnen Transaktion sind in Cloud Datastore beschränkt.

Alle Datastore-Vorgänge in einer Transaktion müssen für die Entitäten in derselben Entitätengruppe ausgeführt werden, wenn es eine Transaktion für nur eine Gruppe ist. Bei einer gruppenübergreifenden Transaktion sind Vorgänge für Entitäten in maximal 25 Entitätengruppen möglich. Hierzu gehören das Abfragen von Entitäten nach Ancestor, das Abrufen von Entitäten nach Schlüssel, das Aktualisieren und das Löschen von Entitäten. Beachten Sie, dass jede Stammentität zu einer separaten Entitätengruppe gehört. Daher kann eine einzelne Transaktion nicht mehr als eine Stammentität erstellen oder bearbeiten, es sei denn, es ist eine gruppenübergreifende Transaktion.

Wenn zwei oder mehr Transaktionen gleichzeitig versuchen, Entitäten in einer oder mehreren gemeinsamen Entitätengruppen zu ändern, ist nur die erste Transaktion, die ihre Änderungen mit Commit speichert, erfolgreich. Alle anderen schlagen beim Speichern mit Commit fehl. Aufgrund dieser Funktionsweise ist bei der Verwendung von Entitätengruppen die Anzahl von gleichzeitigen Schreibvorgängen bei einer Entität in einer Gruppe begrenzt. Beim Start einer Transaktion verwendet Datastore die optimistische Gleichzeitigkeitserkennung. Damit wird die letzte Aktualisierungszeit für die Entitätengruppen in der Transaktion geprüft. Beim Commit einer Transaktion für die Entitätengruppen prüft Datastore noch einmal die letzte Aktualisierungszeit für die Entitätengruppen in der Transaktion. Wenn sich diese seit der anfänglichen Prüfung geändert hat, wird ein Fehler zurückgegeben.

Isolation und Konsistenz

Außerhalb von Transaktionen ist die Isolationsebene von Datastore dem Lesevorgang am ähnlichsten, für den ein Commit durchgeführt wurde. Innerhalb von Transaktionen wird die serialisierbare Isolation erzwungen. Dies bedeutet, dass keine andere Transaktion die Daten gleichzeitig ändern kann, die von dieser Transaktion gelesen oder geändert werden.

In einer Transaktion spiegeln alle Lesevorgänge den aktuellen, konsistenten Status von Datastore zu Beginn der Transaktion wider. Mit Abfragen und Get-Anfragen innerhalb einer Transaktion wird garantiert ein konsistenter Snapshot von Datastore seit dem Beginn der Transaktion gefunden. Entitäten und Indexzeilen in der Entitätengruppe der Transaktion werden vollständig aktualisiert. So geben Abfragen die vollständige, korrekte Menge von Ergebnisentitäten ohne die falsch positiven oder falsch negativen Ergebnisse zurück, die in Abfragen außerhalb von Transaktionen auftreten können.

Diese konsistente Snapshot-Ansicht erstreckt sich innerhalb von Transaktionen auch auf Lesevorgänge nach Schreibvorgängen. Im Gegensatz zu den meisten Datenbanken sehen Abfragen und Abrufe innerhalb einer Datastore-Transaktion die Ergebnisse vorheriger Schreibvorgänge innerhalb dieser Transaktion nicht. Das bedeutet, dass eine Abfrage oder ein Abruf, wenn eine Entität innerhalb einer Transaktion geändert oder gelöscht wird, die ursprüngliche Version der Entität zum Zeitpunkt des Beginns der Transaktion zurückgibt, bzw. nichts, falls die Entität zu diesem Zeitpunkt noch nicht existierte.

Anwendungsfälle für Transaktionen

Dieses Beispiel zeigt eine Verwendung von Transaktionen: Es wird eine Entität mit einem neuen Property-Wert aktualisiert, der sich auf den aktuellen Wert bezieht.

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

Dies erfordert eine Transaktion, weil der Wert möglicherweise von einem anderen Nutzer aktualisiert wird, nachdem dieser Code das Objekt abruft, jedoch bevor das geänderte Objekt gespeichert wird. Ohne eine Transaktion verwendet die Anfrage des Nutzers den Wert von count vor der Aktualisierung durch den anderen Nutzer; beim Speichern wird der neue Wert überschrieben. Mit einer Transaktion wird die Anwendung über die Aktualisierung durch den anderen Nutzer informiert. Wenn die Entität während der Transaktion aktualisiert wird, wird die Transaktion so lange wiederholt, bis alle Schritte ohne Unterbrechung abgeschlossen wurden.

Transaktionen werden häufig auch dazu verwendet, eine Entität mit einem benannten Schlüssel abzurufen oder die Entität zu erstellen, wenn diese noch nicht vorhanden ist.

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

Wie zuvor ist eine Transaktion für den Fall erforderlich, dass ein anderer Nutzer gerade versucht, eine Entität mit derselben String-ID zu erstellen oder zu aktualisieren. Wenn die Entität nicht vorhanden ist und zwei Nutzer gleichzeitig versuchen sie zu erstellen, überschreibt ohne Verwendung einer Transaktion der zweite Versuch unbemerkt den ersten.

Wenn eine Transaktion fehlschlägt, können Sie veranlassen, dass die Anwendung die Transaktion solange wiederholt, bis sie erfolgreich abgeschlossen wird. Sie können aber auch die Nutzer den Fehler beheben lassen, indem Sie sie an die Benutzeroberfläche Ihrer Anwendung weiterleiten. Sie müssen keine Wiederholungsschleife für jede Transaktion erstellen.

Schließlich können Sie eine Transaktion verwenden, um einen konsistenten Snapshot von Datastore zu lesen. Dies kann nützlich sein, wenn mehrere Lesevorgänge erforderlich sind, um eine Seite anzuzeigen oder Daten zu exportieren, die konsistent sein müssen. Diese Arten von Transaktionen werden häufig als schreibgeschützte Transaktionen bezeichnet, weil sie keine Schreibvorgänge ausführen. Schreibgeschützte Transaktionen für nur eine Gruppe schlagen niemals wegen gleichzeitiger Änderungen fehl, sodass Sie keine Wiederholungen bei einem Fehler implementieren müssen. Gruppenübergreifende Transaktionen können jedoch wegen gleichzeitiger Änderungen fehlschlagen, sodass für sie Wiederholungen festgelegt werden müssen. Commit und Rollback sind bei einer Nur-Lese-Transaktion nicht verfügbar.

Transaktionsaufgabe in eine Warteschlange stellen

Sie können eine Aufgabe als Teil einer Datastore-Transaktion in die Warteschlange stellen, sodass die Aufgabe dann – und nur dann – in die Warteschlange aufgenommen wird, wenn der Commit für die Transaktion erfolgreich ausgeführt wurde. Nach der Aufnahme in die Warteschlange wird die Aufgabe nicht unbedingt sofort ausgeführt, sodass die Aufgabe nicht unteilbar mit der Transaktion verbunden ist. Allerdings wird nach der Aufnahme so oft versucht, die Aufgabe auszuführen, bis die Ausführung erfolgreich ist. Dies gilt für jede Aufgabe, die während einer RunInTransaction-Funktion in die Warteschlange eingereiht wird.

Transaktionale Aufgaben sind nützlich, da damit nicht auf den Datenspeicher bezogene Aktionen zu einer Transaktion kombiniert werden können, die davon abhängen, dass die Transaktion erfolgreich ist, beispielsweise der Versand einer E-Mail zur Bestätigung eines Kaufs. Sie können auch Datenspeicheraktionen mit der Transaktion verbinden, um beispielsweise Änderungen an Entitätengruppen außerhalb der Transaktion per Commit festzuschreiben – aber nur, wenn die Transaktion erfolgreich ist.

Eine Anwendung kann während einer einzelnen Transaktion nicht mehr als fünf Transaktionsaufgaben in Aufgabenwarteschlangen einfügen. Transaktionsaufgaben dürfen keine benutzerdefinierten Namen haben.

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)