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

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

Hier sehen Sie ein Beispiel für die Aktualisierung des Felds vacationDays in einer Entität des Typs Employee mit dem Namen Joe:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Transaction txn = datastore.beginTransaction();
try {
  Key employeeKey = KeyFactory.createKey("Employee", "Joe");
  Entity employee = datastore.get(employeeKey);
  employee.setProperty("vacationDays", 10);

  datastore.put(txn, employee);

  txn.commit();
} finally {
  if (txn.isActive()) {
    txn.rollback();
  }
}

Beachten Sie: Um unsere Beispiele prägnanter zu machen, lassen wir zuweilen den finally-Block weg, der ein Rollback durchführt, wenn die Transaktion noch aktiv ist. Beim Produktionscode muss jede Transaktion entweder explizit in einem Commit-Vorgang bestätigt oder ein Rollback dafür durchgeführt werden.

Entitätengruppen

Jede Entität gehört zu einer Entitätengruppe. Dabei handelt es sich um einen Satz mit mindestens einer Entität, der mit einer einzelnen Transaktion geändert werden kann. Durch Entitätengruppenbeziehungen wird App Engine angewiesen, mehrere Entitäten im gleichen Teil des verteilten Netzwerks zu speichern. Mit einer Transaktion werden Datenspeichervorgänge für Entitätsgruppen eingerichtet. Alle Vorgänge werden als Gruppe angewendet. Es werden also keine Vorgänge angewendet, wenn bei der Transaktion ein Fehler auftritt.

Beim Erstellen einer Entität in einer Anwendung kann der neuen Entität eine andere Entität als übergeordnete Entität zugeordnet werden. Durch das Zuordnen einer übergeordneten Entität zu einer neuen Entität gehört die neue Entität derselben Entitätengruppe wie die übergeordnete Entität an.

Eine Entität ohne übergeordnete Entität wird als Stammentität bezeichnet. Eine Entität, die als übergeordnetes Element einer anderen Entität fungiert, kann ebenfalls ein übergeordnetes Element haben. Eine Kette übergeordneter Entitäten, von einer Entität bis hin zum Stamm, wird als Pfad der Entität bezeichnet. Die Elemente des Pfads sind die Ancestors der Entität. Das übergeordnete Element einer Entität wird beim Erstellen der Entität definiert und kann später nicht mehr geändert werden.

Jede Entität mit einer bestimmten Stammentität als Ancestor befindet sich in derselben Entitätengruppe. Alle Entitäten in einer Gruppe werden im gleichen Datenspeicherknoten gespeichert. Mit einer einzelnen Transaktion können mehrere Entitäten in einer einzelnen Gruppe geändert oder neue Entitäten zur Gruppe hinzugefügt werden, indem das übergeordnete Element der neuen Entität zu einer vorhandenen Entität in der Gruppe gemacht wird. Der folgende Code-Snippet zeigt Transaktionen für verschiedene Entitätstypen:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Entity person = new Entity("Person", "tom");
datastore.put(person);

// Transactions on root entities
Transaction txn = datastore.beginTransaction();

Entity tom = datastore.get(person.getKey());
tom.setProperty("age", 40);
datastore.put(txn, tom);
txn.commit();

// Transactions on child entities
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photo = new Entity("Photo", tom.getKey());

// Create a Photo that is a child of the Person entity named "tom"
photo.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photo);
txn.commit();

// Transactions on entities in different entity groups
txn = datastore.beginTransaction();
tom = datastore.get(person.getKey());
Entity photoNotAChild = new Entity("Photo");
photoNotAChild.setProperty("photoUrl", "http://domain.com/path/to/photo.jpg");
datastore.put(txn, photoNotAChild);

// Throws IllegalArgumentException because the Person entity
// and the Photo entity belong to different entity groups.
txn.commit();

Entität in einer bestimmten Entitätengruppe erstellen

Wenn Ihre Anwendung eine neue Entität erstellt, können Sie sie einer Entitätengruppe zuweisen, indem Sie den Schlüssel einer anderen Entität bereitstellen. Im folgenden Beispiel wird der Schlüssel der Entität MessageBoard erstellt. Anschließend wird dieser Schlüssel verwendet, um die Entität Message, die sich in derselben Entitätengruppe befindet wie die Entität MessageBoard, zu erstellen und persistent zu machen:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

String messageTitle = "Some Title";
String messageText = "Some message.";
Date postDate = new Date();

Key messageBoardKey = KeyFactory.createKey("MessageBoard", boardName);

Entity message = new Entity("Message", messageBoardKey);
message.setProperty("message_title", messageTitle);
message.setProperty("message_text", messageText);
message.setProperty("post_date", postDate);

Transaction txn = datastore.beginTransaction();
datastore.put(txn, message);

txn.commit();

Gruppenübergreifende Transaktionen verwenden

Gruppenübergreifende Transaktionen (auch XG-Transaktionen genannt) werden über mehrere Entitätengruppen hinweg ausgeführt und verhalten sich wie oben beschriebene Transaktionen für nur eine Gruppe, mit dem Unterschied, dass gruppenübergreifende Transaktionen nicht fehlschlagen, wenn aufgrund des Codes Entitäten aus mehreren Entitätengruppen aktualisiert werden sollen.

Gruppenübergreifende Transaktionen werden ähnlich verwendet wie Transaktionen für nur eine einzelne Gruppe. Sie als Nutzer müssen lediglich beim Starten der Transaktion mit TransactionOptions angeben, dass die Transaktion gruppenübergreifend sein soll:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
TransactionOptions options = TransactionOptions.Builder.withXG(true);
Transaction txn = datastore.beginTransaction(options);

Entity a = new Entity("A");
a.setProperty("a", 22);
datastore.put(txn, a);

Entity b = new Entity("B");
b.setProperty("b", 11);
datastore.put(txn, b);

txn.commit();

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.

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. Da die Datastore API keine Transaktionen wiederholt, können wir Logik hinzufügen, damit die Transaktion wiederholt wird, wenn eine andere Anfrage dasselbe MessageBoard oder eine seiner Messages zur gleichen Zeit aktualisiert.

int retries = 3;
while (true) {
  Transaction txn = datastore.beginTransaction();
  try {
    Key boardKey = KeyFactory.createKey("MessageBoard", boardName);
    Entity messageBoard = datastore.get(boardKey);

    long count = (Long) messageBoard.getProperty("count");
    ++count;
    messageBoard.setProperty("count", count);
    datastore.put(txn, messageBoard);

    txn.commit();
    break;
  } catch (ConcurrentModificationException e) {
    if (retries == 0) {
      throw e;
    }
    // Allow retry to occur
    --retries;
  } finally {
    if (txn.isActive()) {
      txn.rollback();
    }
  }
}

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, schlägt die Transaktion mit ConcurrentModificationException fehl. Die Transaktion kann von der Anwendung wiederholt werden, um die neuen Daten zu verwenden.

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.

Transaction txn = datastore.beginTransaction();
Entity messageBoard;
Key boardKey;
try {
  boardKey = KeyFactory.createKey("MessageBoard", boardName);
  messageBoard = datastore.get(boardKey);
} catch (EntityNotFoundException e) {
  messageBoard = new Entity("MessageBoard", boardName);
  messageBoard.setProperty("count", 0L);
  boardKey = datastore.put(txn, messageBoard);
}
txn.commit();

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. Mit einer Transaktion schlägt der zweite Versuch unteilbar fehl. Sofern dies sinnvoll ist, kann die Anwendung versuchen, die Entität noch einmal abzurufen und zu aktualisieren.

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 aber auch die Nutzer den Fehler verarbeiten lassen, indem Sie ihn 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.

DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

// Display information about a message board and its first 10 messages.
Key boardKey = KeyFactory.createKey("MessageBoard", boardName);

Transaction txn = datastore.beginTransaction();

Entity messageBoard = datastore.get(boardKey);
long count = (Long) messageBoard.getProperty("count");

Query q = new Query("Message", boardKey);

// This is an ancestor query.
PreparedQuery pq = datastore.prepare(txn, q);
List<Entity> messages = pq.asList(FetchOptions.Builder.withLimit(10));

txn.commit();

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. Wenn die Transaktion mit Commit gespeichert wird, wird die Aufgabe garantiert in die Warteschlange eingereiht. Nach der Einreihung in die Warteschlange wird die Aufgabe nicht garantiert sofort ausgeführt und alle Vorgänge, die innerhalb der Aufgabe durchgeführt werden, erfolgen unabhängig von der ursprünglichen Transaktion. Die Durchführung der Aufgabe wird so oft wiederholt, bis sie erfolgreich ist. Dies gilt für jede Aufgabe, die im Rahmen einer Transaktion in die Warteschlange eingereiht wird.

Transaktionsaufgaben sind nützlich, da Sie damit nicht auf den Datenspeicher bezogene Aktionen in eine Datenspeichertransaktion aufnehmen können, beispielsweise den Versand einer E-Mail zur Bestätigung eines Kaufs. Sie können auch Datenspeicheraktionen mit der Transaktion verknüpfen, beispielsweise die Bestätigung von Änderungen an zusätzlichen Entitätsgruppen außerhalb einer Transaktion immer dann und nur dann, 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 vom Nutzer angegebenen Namen haben.

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Queue queue = QueueFactory.getDefaultQueue();
Transaction txn = datastore.beginTransaction();
// ...

queue.add(txn, TaskOptions.Builder.withUrl("/path/to/handler"));

// ...

txn.commit();