Transaktionen

Hinweis: Entwicklern von neuen Anwendungen wird dringend empfohlen, die NDB-Clientbibliothek zu verwenden. Diese bietet im Vergleich zur vorliegenden Clientbibliothek verschiedene Vorteile, z. B. das automatische Caching von Entitäten über die Memcache API. Wenn Sie derzeit die ältere DB-Clientbibliothek verwenden, finden Sie weitere Informationen im Leitfaden zur Migration von DB- zu NDB-Clientbibliotheken.

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 löst die Cloud Datastore API eine Ausnahme aus.

Transaktionen sind ein optionales Feature von Datastore und müssen für Datastore-Vorgänge nicht zwingend verwendet werden.

In Anwendungen können mehrere Anweisungen und Datastore-Vorgänge mit nur einer Transaktion ausgeführt werden. Dies bewirkt, dass keiner der entsprechenden Datastore-Vorgänge angewendet wird, wenn bei einer Anweisung oder einem Vorgang eine Ausnahme ausgegeben wird. Die in der Transaktion auszuführenden Aktionen werden mit einer Python-Funktion in der Anwendung definiert. Die Anwendung startet die Transaktion mit einer der run_in_transaction-Methoden, je nachdem, ob die Transaktion auf Entitäten innerhalb einer einzelnen Entitätengruppe zugreift oder ob es eine gruppenübergreifende Transaktion ist.

Für den häufigen Anwendungsfall, in dem eine Funktion nur innerhalb von Transaktionen verwendet wird, sollten Sie den Decorator @db.transactional verwenden:

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

@db.transactional
def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

increment_counter(acc.key(), 5)

Wenn die Funktion ohne eine Transaktion aufgerufen werden soll, rufen Sie db.run_in_transaction() mit der Funktion als Argument auf, statt sie zu dekorieren:

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

db.run_in_transaction(increment_counter, acc.key(), 5)

db.run_in_transaction() verwendet das Funktionsobjekt sowie Positions- und Keyword-Argumente, die an die Funktion übergeben werden. Wenn die Funktion einen Wert zurückgibt, gibt db.run_in_transaction() diesen Wert zurück.

Die Transaktion wird per Commit bestätigt, wenn die Funktion Werte zurückgibt. Alle Auswirkungen der Datastore-Vorgänge werden angewendet. Löst die Funktion eine Ausnahme aus, wird die Transaktion zurückgesetzt und die Effekte werden nicht angewendet. Beachten Sie den obigen Hinweis zu Ausnahmen.

Wenn eine Transaktionsfunktion innerhalb einer anderen Transaktion aufgerufen wird, haben @db.transactional und db.run_in_transaction() ein anderes Standardverhalten. Dies wird von @db.transactional zugelassen und die innere Transaktion wird dieselbe Transaktion wie die äußere. Durch den Aufruf von db.run_in_transaction() wird versucht, eine andere Transaktion in der vorhandenen Transaktion zu „verschachteln“. Dieses Verhalten wird jedoch noch nicht unterstützt und löst den Fehler db.BadRequestError aus. Sie können ein anderes Verhalten vorgeben. Einzelheiten dazu finden Sie in der Funktionsreferenz zu Transaktionsoptionen.

Gruppenübergreifende Transaktionen (XG) verwenden

Gruppenübergreifende Transaktionen werden für mehrere Entitätengruppen ausgeführt und verhalten sich wie Transaktionen für eine einzige Gruppe. Sie schlagen jedoch nicht fehl, wenn aufgrund des Codes Entitäten aus mehr als einer Entitätengruppe aktualisiert werden sollen. Zum Aufrufen einer gruppenübergreifenden Transaktion müssen Sie Transaktionsoptionen verwenden.

mit @db.transactional:

from google.appengine.ext import db

@db.transactional(xg=True)
def make_things():
  thing1 = Thing(a=3)
  thing1.put()
  thing2 = Thing(a=7)
  thing2.put()

make_things()

mit db.run_in_transaction_options:

from google.appengine.ext import db

xg_on = db.create_transaction_options(xg=True)

def my_txn():
    x = MyModel(a=3)
    x.put()
    y = MyModel(a=7)
    y.put()

db.run_in_transaction_options(xg_on, my_txn)

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 sie sich seit der ersten Überprüfung geändert hat, wird eine Ausnahme ausgelöst.

Eine Anwendung kann während einer Transaktion nur dann eine Abfrage durchführen, wenn sie einen Ancestor-Filter enthält. Außerdem kann eine Anwendung während einer Transaktion Datastore-Entitäten nach Schlüssel abrufen. Sie können Schlüssel vor der Transaktion vorbereiten oder innerhalb der Transaktion aus Schlüsselnamen oder IDs erstellen.

Sämtlicher anderer Python-Code ist in Transaktionsfunktionen zulässig. Mit db.is_in_transaction() können Sie feststellen, ob der aktuelle Bereich in einer Transaktionsfunktion verschachtelt ist. Die Transaktionsfunktion sollte neben den Datastore-Vorgängen keine anderen Auswirkungen haben. Sollte ein Datastore-Vorgang fehlschlagen, weil ein anderer Nutzer gleichzeitig Entitäten in der Entitätengruppe aktualisiert, kann die Transaktionsfunktion mehrmals aufgerufen werden. In diesem Fall versucht die Datastore API mit einer festgelegten Häufigkeit, die Transaktion noch einmal auszuführen. Schlagen alle Versuche fehl, löst db.run_in_transaction() den Fehler TransactionFailedError aus. Sie können die Häufigkeit, mit der die Transaktion wiederholt werden soll, mithilfe von db.run_in_transaction_custom_retries() anstelle von db.run_in_transaction() anpassen.

Gleichermaßen sollte die Transaktionsfunktion keine Nebeneffekte haben, die vom Erfolg der Transaktion abhängen, sofern im Code, mit dem die Transaktionsfunktion aufgerufen wird, keine Informationen darüber vorliegen, wie diese Effekte wieder aufgehoben werden können. Wenn mit der Transaktion zum Beispiel eine neue Datastore-Entität sowie die ID der erstellten Entität für eine spätere Verwendung gespeichert wird und bei der Transaktion dann ein Fehler auftritt, verweist die gespeicherte ID nicht auf die vorgesehene ID, da die Erstellung der Entität zurückgesetzt wurde. In diesem Fall sollte die gespeicherte ID nicht im aufrufenden Code verwendet werden.

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: die Aktualisierung einer Entität mit einem neuen Attributwert, der sich auf den aktuellen Wert bezieht.

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

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.

class SalesAccount(db.Model):
    address = db.PostalAddressProperty()
    phone_number = db.PhoneNumberProperty()

def get_or_create(parent_key, account_id, address, phone_number):
    obj = db.get(db.Key.from_path("SalesAccount", account_id, parent=parent_key))
    if not obj:
        obj = SalesAccount(key_name=account_id,
                           parent=parent_key,
                           address=address,
                           phone_number=phone_number)
        obj.put()
    else:
        obj.address = address
        obj.phone_number = phone_number

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 eine Entität nicht existiert und zwei Nutzer gleichzeitig versuchen, sie zu erstellen, dann überschreibt ohne Verwendung einer Transaktion der zweite Versuch den ersten, ohne dass der erste Nutzer dies bemerkt. Mit einer Transaktion wird der zweite Versuch wiederholt. Dabei wird festgestellt, dass die Entität existiert, weshalb sie aktualisiert statt überschrieben wird.

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

Da das Abfragen oder Erstellen äußerst nützlich ist, gibt es dafür eine eingebundene Methode. Model.get_or_insert() übergibt einen Schlüsselnamen, ein optionales übergeordnetes Element und Argumente an den Modellkonstruktor, wenn keine Entität mit diesem Namen und Pfad vorhanden ist. Der Abrufversuch und der Erstellvorgang finden in einer Transaktion statt, weshalb die Methode – wenn die Transaktion erfolgreich ist – immer eine Modellinstanz zurückgibt, die eine tatsächliche Entität darstellt.

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.

class Customer(db.Model):
    user = db.StringProperty()

class Account(db.Model):
    """An Account has a Customer as its parent."""
    address = db.PostalAddressProperty()
    balance = db.FloatProperty()

def get_all_accounts():
    """Returns a consistent view of the current user's accounts."""
    accounts = []
    for customer in Customer.all().filter('user =', users.get_current_user().user_id()):
        accounts.extend(Account.all().ancestor(customer))
    return accounts

Transaktionsaufgabe in eine Warteschlange stellen

Sie können eine Aufgabe im Rahmen einer Datastore-Transaktion in die Warteschlange stellen. Die Aufgabe wird in diesem Fall nur bei einem erfolgreichen Commit der Transaktion in die Warteschlange gestellt. Wenn die Transaktion nicht mit Commit gespeichert wird, wird die Aufgabe nicht in die Warteschlange eingereiht. Wird die Transaktion mit Commit gespeichert, erfolgt die Aufnahme der Aufgabe in die Warteschlange. Nach der Aufnahme in die Warteschlange wird die Aufgabe nicht sofort ausgeführt, sodass sie nicht unteilbar mit der Transaktion verbunden ist. Allerdings wird nach der Einreihung so oft versucht, die Aufgabe auszuführen, bis die Ausführung erfolgreich ist. Dies gilt für jede Aufgabe, die innerhalb einer run_in_transaction()-Funktion in die Warteschlange eingereiht wird.

Transaktionsaufgaben sind hilfreich, da Sie damit Datastore-externe Vorgänge mit einer Transaktion verbinden können, sodass die Ausführung vom Erfolg der Transaktion abhängig ist. Dies eignet sich beispielsweise zum Senden einer E-Mail-Bestätigung nach einem Kauf. Sie können auch Datastore-Vorgänge mit der Transaktion verbinden, um beispielsweise Änderungen an Entitätengruppen außerhalb der Transaktion nur dann per Commit festzuschreiben, wenn die Transaktion erfolgreich ist.

Von einer Anwendung können während einer einzelnen Transaktion nicht mehr als fünf Transaktionsaufgaben in Aufgabenwarteschlangen eingefügt werden. Transaktionsaufgaben dürfen keine benutzerdefinierten Namen haben.

def do_something_in_transaction(...)
    taskqueue.add(url='/path/to/my/worker', transactional=True)
  ...

db.run_in_transaction(do_something_in_transaction, ....)