Async Datastore API

Mit der Async Datastore API können Sie parallele, nicht blockierende Aufrufe an den Datenspeicher ausführen und die Ergebnisse später bei der Verarbeitung der Anfrage abrufen. In dieser Dokumentation werden die folgenden Aspekte der Async Datastore API erläutert:

Mit dem Async Datastore-Dienst arbeiten

Mit der Async Datastore API führen Sie Datastore-Aufrufe mit den Methoden der Schnittstelle AsyncDatastoreService durch. Sie erhalten dieses Objekt, wenn Sie die Klassenmethode getAsyncDatastoreService() der Klasse DatastoreServiceFactory aufrufen:

import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;

// ...
AsyncDatastoreService datastore = DatastoreServiceFactory.getAsyncDatastoreService();

AsyncDatastoreService unterstützt dieselben Vorgänge wie DatastoreService, mit dem Unterschied, dass die meisten Methoden sofort ein Future zurückgeben, dessen Ergebnis Sie später blockieren können. DatastoreService.get() gibt beispielsweise eine Entität zurück. Bei AsyncDatastoreService.get() ist es ein Future<Entity>:

// ...

Key key = KeyFactory.createKey("Employee", "Max");
// Async call returns immediately
Future<Entity> entityFuture = datastore.get(key);

// Do other stuff while the get operation runs in the background...

// Blocks if the get operation has not finished, otherwise returns instantly
Entity entity = entityFuture.get();

Hinweis: Ausnahmen werden erst ausgelöst, wenn Sie die get()-Methode aufrufen. Durch das Aufrufen dieser Methode können Sie überprüfen, ob der asynchrone Vorgang erfolgreich war.

Wenn Sie einen AsyncDatastoreService haben, aber einen Vorgang synchron ausführen möchten, rufen Sie die geeignete AsyncDatastoreService-Methode auf und blockieren anschließend sofort das Ergebnis:

// ...

Entity entity = new Employee("Employee", "Alfred");
// ... populate entity properties

// Make a sync call via the async interface
Key key = datastore.put(key).get();

Mit asynchronen Transaktionen arbeiten

Aufrufe der Async Datastore API können genau wie synchrone Aufrufe an Transaktionen beteiligt sein. Bei dieser Funktion wird im Rahmen einer einzelnen Transaktion das Gehalt von Employee angepasst und eine zusätzliche Entität SalaryAdjustment in dieselbe Entitätengruppe wie Employee geschrieben:

void giveRaise(AsyncDatastoreService datastore, Key employeeKey, long raiseAmount)
        throws Exception {
    Future<Transaction> txn = datastore.beginTransaction();

    // Async call to lookup the Employee entity
    Future<Entity> employeeEntityFuture = datastore.get(employeeKey);

    // Create and put a SalaryAdjustment entity in parallel with the lookup
    Entity adjustmentEntity = new Entity("SalaryAdjustment", employeeKey);
    adjustmentEntity.setProperty("adjustment", raiseAmount);
    adjustmentEntity.setProperty("adjustmentDate", new Date());
    datastore.put(adjustmentEntity);

    // Fetch the result of our lookup to make the salary adjustment
    Entity employeeEntity = employeeEntityFuture.get();
    long salary = (Long) employeeEntity.getProperty("salary");
    employeeEntity.setProperty("salary", salary + raiseAmount);

    // Re-put the Employee entity with the adjusted salary.
    datastore.put(employeeEntity);
    txn.get().commit(); // could also call txn.get().commitAsync() here
}

Dieses Beispiel veranschaulicht einen wichtigen Unterschied zwischen asynchronen Aufrufen ohne Transaktionen und asynchronen Aufrufen mit Transaktionen. Wenn Sie keine Transaktion verwenden, kann nur sichergestellt werden, dass ein einzelner asynchroner Aufruf abgeschlossen wurde, indem der Rückgabewert des Future abgerufen wird, das bei der Durchführung des Aufrufs zurückgegeben wurde. Wenn Sie eine Transaktion verwenden, werden durch das Aufrufen von Transaction.commit() vor dem Commit die Ergebnisse aller asynchronen Aufrufe blockiert, die seit dem Start der Transaktion erfolgt sind.

Für unser obiges Beispiel bedeutet dies: Auch wenn unser asynchroner Aufruf zur Einfügung der SalaryAdjustment-Entität vielleicht noch aussteht, wenn wir commit() aufrufen, wird der Commit-Vorgang erst nach Abschluss der Einfügung durchgeführt. Ähnliches gilt, wenn Sie commitAsync() statt commit() aufrufen. Dann wird der Aufruf von get() für das Future, das von commitAsync() zurückgegeben wurde, blockiert, bis alle ausstehenden asynchronen Aufrufe abgeschlossen sind.

Hinweis: Transaktionen werden einem bestimmten Thread zugeordnet, nicht einer bestimmten Instanz von DatastoreService oder AsyncDatastoreService. Wenn Sie also eine Transaktion mit einem DatastoreService initiieren und einen asynchronen Aufruf mit einem AsyncDatastoreService durchführen, nimmt der asynchrone Aufruf an der Transaktion teil. Oder einfacher gesagt: DatastoreService.getCurrentTransaction() und AsyncDatastoreService.getCurrentTransaction() geben immer dieselbe Transaction zurück.

Mit Futures arbeiten

Im Future-Javadoc erfahren Sie, wie Sie mit einem Future arbeiten, das von der Async Datastore API zurückgegeben wird. Außerdem gilt es, diese Aspekte von App Engine zu beachten:

Asynchrone Abfragen

Wir bieten derzeit keine explizit asynchrone API für Abfragen. Wenn Sie jedoch PreparedQuery.asIterable(), PreparedQuery.asIterator() oder PreparedQuery.asList(FetchOptions fetchOptions) aufrufen, antworten sowohl DatastoreService als auch AsyncDatastoreService sofort und rufen asynchron Ergebnisse ab. Dadurch kann Ihre Anwendung Aufgaben parallel abarbeiten, während die Abfrageergebnisse abgerufen werden:

// ...

Query q1 = new Query("Salesperson");
q1.setFilter(new FilterPredicate("dateOfHire", FilterOperator.LESS_THAN, oneMonthAgo));

// Returns instantly, query is executing in the background.
Iterable<Entity> recentHires = datastore.prepare(q1).asIterable();

Query q2 = new Query("Customer");
q2.setFilter(new FilterPredicate("lastContact", FilterOperator.GREATER_THAN, oneYearAgo));

// Also returns instantly, query is executing in the background.
Iterable<Entity> needsFollowup = datastore.prepare(q2).asIterable();

schedulePhoneCall(recentHires, needsFollowUp);

Einsatzmöglichkeiten für Async Datastore-Aufrufe

Die von der DatastoreService-Schnittstelle bereitgestellten Vorgänge sind synchron. Wenn Sie beispielsweise DatastoreService.get() aufrufen, wird Ihr Code blockiert, bis der Aufruf beim Datenspeicher abgeschlossen ist. Wenn Ihre Anwendung das Ergebnis von get() einfach nur in HTML ausgeben muss, ist eine Blockierung, bis der Aufruf abgeschlossen ist, eine sinnvolle Vorgehensweise. Falls Ihre Anwendung jedoch sowohl das Ergebnis von get() als auch das Ergebnis einer Query benötigt, um die Antwort auszugeben, und wenn zwischen get() und der Query keine Datenabhängigkeiten bestehen, ist es nicht sinnvoll, auf den Abschluss von get() zu warten, um die Query zu starten. Hier sehen Sie ein Beispiel für Code, der durch die Verwendung der Async API verbessert werden kann:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Key empKey = KeyFactory.createKey("Employee", "Max");

// Read employee data from the Datastore
Entity employee = datastore.get(empKey); // Blocking for no good reason!

// Fetch payment history
Query query = new Query("PaymentHistory");
PreparedQuery pq = datastore.prepare(query);
List<Entity> result = pq.asList(FetchOptions.Builder.withLimit(10));
renderHtml(employee, result);

Sie müssen nicht darauf warten, dass get() abgeschlossen wird. Führen Sie stattdessen mit dem AsyncDatastoreService einen asynchronen Aufruf aus:

AsyncDatastoreService datastore = DatastoreServiceFactory.getAsyncDatastoreService();
Key empKey = KeyFactory.createKey("Employee", "Max");

// Read employee data from the Datastore
Future<Entity> employeeFuture = datastore.get(empKey); // Returns immediately!

// Fetch payment history for the employee
Query query = new Query("PaymentHistory", empKey);
PreparedQuery pq = datastore.prepare(query);

// Run the query while the employee is being fetched
List<Entity> result = pq.asList(FetchOptions.Builder.withLimit(10));
// Implicitly performs query asynchronously
Entity employee = employeeFuture.get(); // Blocking!
renderHtml(employee, result); 

Die synchrone und die asynchrone Version dieses Codes weisen einen ähnlichen CPU-Verbrauch auf, sie führen ja schließlich jeweils denselben Arbeitsumfang aus. Aber da die beiden Datenspeichervorgänge bei der asynchronen Version parallel ausgeführt werden können, weist die asynchrone Version eine geringere Latenz auf. Allgemein gilt: Wenn Sie mehrere Datenspeichervorgänge ohne Datenabhängigkeiten durchführen müssen, kann der AsyncDatastoreService die Latenz erheblich verbessern.