Kolumne

EnterpriseTales: Reaktiv bis zur Datenbank

Arne Limburg

© S&S_Media

Für Reactive Programming gibt es in Java mittlerweile viele Frameworks. Aber wie greife ich dann auf eine relationale Datenbank zu? Der Java-Standard zum Zugriff auf relationale Datenbanken, JDBC, unterstützt bisher nur blockierende Aufrufe. Welche Möglichkeiten gibt es aber dann, wenn man ganz auf Reactive Programming setzt?

In vergangenen Kolumnen habe ich bereits mehrfach über Reactive Programming geschrieben; teilweise war ich dabei durchaus kritisch. Reactive Programming erhöht den Entwicklungs- und Wartungsaufwand, weil Logging, Testing und Debugging deutlich aufwendiger werden. Die Entscheidung, eine Anwendung reactive zu realisieren, macht also nur dann Sinn, wenn die Performance- und/oder Speicheranforderungen so hoch sind, dass normale imperative oder objektorientierte Programmierung nicht ausreicht. Lediglich dann ist der höhere Entwicklungs- und Wartungsaufwand zu rechtfertigen.

Solche Anforderungen kommen häufig zustande, wenn es darum geht, große Mengen an Daten bereitzustellen oder zu verarbeiten. Bei klassischen Entwicklungsansätzen würden diese Daten zunächst komplett in den Speicher geladen und dann entweder zum Client zurückgegeben oder verarbeitet werden.

Der Teufelskreis aus Speicherbedarf und Kontextwechseln
Werden die Datenmengen zu groß, um sie komplett in den Speicher zu laden, würde man klassischerweise damit beginnen, die Daten häppchenweise (z. B. über Pagination) zu verarbeiten. Dieser Ansatz hat allerdings einen Nachteil: Um die Menge der Daten im Speicher gering zu halten, gibt es viele Round-Trips zur Datenbank. Bei einer relationalen Datenbank und der Verwendung von JDBC (sei es direkt oder über JPA) ist allerdings jeder dieser Round-Trips mit einem blockierenden Warten auf das Ergebnis verbunden. Und jeder dieser blockierenden Aufrufe führt zu einem schwergewichtigen Kontextwechsel im Betriebssystem. Dadurch wird die gesamte Verarbeitung inperformant. Um die Anzahl der Kontextwechsel zu minimieren, müsste die Anzahl der Round-Trips minimiert werden, folglich müsste man größere Chunks verwenden. Das wiederum führt allerdings zu mehr Speicherbedarf. Man befände sich in einem Teufelskreis der Optimierung von Datenzugriffen.

Hier kommt Reactive Programming mit Reactive Streams ins Spiel. Anstatt die Daten Chunk-weise zu laden, können sie (nicht-blockierend) gestreamt werden. Die Datenbank informiert die Anwendung, sobald ein neuer Datensatz geladen wurde, woraufhin die Anwendung diesen verarbeiten kann. Damit lassen sich beide Probleme gleichzeitig lösen: Es befindet sich immer nur ein Datensatz im Speicher und es muss nicht blockierend auf die Datenbank gewartet werden. Mit NoSQL-Datenbanken wie z. B. MongoDB ist ein solches Streamen schon länger möglich, wie das folgende Beispiel zeigt.

MongoCollection<Document> customers
  = database.getCollection("customers");
Publisher<Document> customerStream = customers.find();
customerStream.subscribe(customerProcessor);

Aber wie sieht es bei relationalen Datenbanken aus? Das Streamen von Daten ist auch hier möglich. Im JDBC-Standard gibt es dafür das ScrollableResultSet. Dieses ermöglicht es, Daten einzeln in den Speicher zu laden. Was allerdings fehlt, ist die Asynchronität. Auf jeden einzelnen Datensatz muss hier blockierend gewartet werden, was wieder zu den inperformanten Kontextwechseln führt.

Ist eine asynchrone Verarbeitung mit relationalen Datenbanken also nicht möglich? Das ist zum Glück nicht so. Die meisten relationalen Datenbanken unterstützen das asnychrone Speichern und Laden von Daten. Was fehlt, ist die standardisierte Anbindung an Java. JDBC kennt hier nur blockierende Aufrufe.

Asynchrone Kommunikation mit den Treibern der Hersteller

Da der JDBC-Standard keine Möglichkeit der asynchronen Kommunikation bietet, lohnt sich ein Blick auf die Zusatz-APIs, die die Hersteller mit ihren JDBC-Treibern ausliefern.

In MySQL z. B. gibt es das X DevAPI. Dieses bietet erweiterte Möglichkeiten für den Zugriff auf MySQL-Datenbanken. So kann unter anderem eine Datenbankabfrage asynchron abgesetzt werden (Listing 1).

Table customers = database.getTable("customers");
CompletableFuture customerResult
  = customers.select("firstName", "lastName").executeAsync();
SubmissionPublisher customerStream = new SubmissionPublisher<>();
customerResult.thenAccept(rowResult -> {
  while (rowResult.hasNext()) {
    customerStream.submit(rowResult.fetchOne());
  }
});
customerStream.subscribe(customerProcessor);

Was mit diesem API leider (noch) nicht möglich ist, ist das asynchrone Streamen großer Ergebnismengen. Zwar können die Datenbankzeilen einzeln geladen werden (mit fetchOne()), der Aufruf von fetchOne() ist allerdings wieder synchron und damit blockierend.

Reaktiver Datenbankzugriff mit alternativen Treibern

Einige Hersteller wie z. B. PostgreSQL haben die Protokolle, über die mit der Datenbank kommuniziert werden kann, sehr gut dokumentiert. Im Fall von PostgreSQL ist über das Protokoll auch asynchrone Kommunikation möglich, sodass auf dieser Basis ein reaktiver Treiber implementiert werden kann. Tatsächlich gibt es auch verschiedene Open-Source-Projekte, die genau das tun. Mit diesen Treibern kann somit eine asynchrone Datenbankabfrage abgesetzt und das Ergebnis gestreamt werden (Listing 2).

Observable<Row> customerStream = Client
  .rxBegin() // cursors require a transaction
  .flatMapObservable(tx -> tx
    .rxPrepare("SELECT * FROM customers")
    .flatMapObservable(query -> query
      .createStream(1, Tuple.tuple())
      .toObservable())
    .doAfterTerminate(tx::commit));
customerStream.subscribe(customerProcessor);

ADBA mit JDBC Wrapper für den Übergang?

Geht man den beschriebenen Weg von herstellerspezifischen APIs oder Open-Source-Projekten, verlässt man natürlich das sichere Pflaster der Standardisierung und damit der garantierten Abwärtskompatibilität. Da ein Wechsel des Datenbankherstellers in der Praxis eher selten vorkommt, ist das aber ein überschaubares Risiko, das durchaus eingegangen werden kann.

Dennoch wäre es schön, wenn es einen Java-Standard für asynchrone Datenbankkommunikation gäbe. Ein solcher Standard ist mit ADBA bereits in Arbeit, wie Sven Kölpin in seiner Kolumne berichtete. Die Kolumne ist mittlerweile allerdings fast ein Jahr alt und ein Release von ADBA leider noch immer nicht in Sicht.

Immerhin gibt es mittlerweile das Projekt AoJ, also „ADBA over JDBC“, das die Möglichkeit bietet, den aktuellen Stand der ADBA-Spezifikation zu verproben und im Hintergrund einen JDBC-Treiber zu verwenden. Dennoch ist dieses Projekt nicht für den Produktivbetrieb vorgesehen. Die tatsächlichen Vorteile von asynchroner Datenbankkommunikation kann es auch nicht bieten, da im Hintergrund weiterhin JDBC verwendet wird. Damit existiert wiederum blockierender Code; dieser wird nur in einen separaten Thread ausgelagert.

Allerdings ermöglicht das Projekt, das neue ADBA API einmal auszuprobieren und dabei mit einer echten Datenbank zu sprechen.

R2DBC als alternative Abstraktion?

Das Spring-Framework steht seit jeher für kurze Innovationszyklen und ist nicht dafür bekannt, auf die Fertigstellung eines Java-Standards zu warten. In der Regel gibt es stattdessen eine eigene Lösung, die dann ggf. später den Standard adaptiert.

Und so ist es auch beim reaktiven Zugriff auf relationale Datenbanken. Da die Spezifikation des ADBA-Standards noch andauert, wurde das Projekt R2DBC ins Leben gerufen.

R2DBC bietet ein API zum reaktiven Zugriff auf relationale Datenbanken. Letztendlich handelt es sich dabei also um einen alternativen Treiber, wie er bereits oben für PostgreSQL beschrieben wurde. Im Vergleich zu diesem hat man aber zwei Vorteile: Erstens erhält man eine sehr gute Integration in das Spring-Framework, genauer gesagt in Spring Data und Project Reactor. Die Erstellung von Reactive Repositories passiert damit automatisch. Wer Spring sowieso nutzt, sollte R2DBC ggf. allein deshalb schon den beschriebenen anderen Lösungen vorziehen.

Der zweite Vorteil von R2DBC ergibt sich aus der Tatsache, dass das Projekt wie ein Standard aufgezogen wurde, d. h., es gibt ein Public API, das von verschiedenen Treibern implementiert werden kann und wird. Zusätzlich gibt es eine Test-Suite, die eine Überprüfung ermöglicht, ob eine Implementierung dem API entspricht. Das erleichtert die Anbindung unterschiedlicher Datenbanken und sei es nur, um eine In-Memory-Datenbank wie H2 in den Tests zu verwenden und z. B. PostgreSQL in Produktion.

Mittlerweile existieren R2DBC-Implementierungen für PostgreSQL, MS SQL Server und H2. Zudem gibt es ein Projekt, das ADBA wrappt. Dadurch kann R2DBC direkt weiterverwendet werden, wenn der ADBA-Standard veröffentlicht worden ist. Außerdem könnte mit dem oben erwähnten AoJ-Treiber bereits jetzt jede beliebige Datenbank mit JDBC-Treibern angebunden werden, dann allerdings doppelt gewrappt (zunächst mit AoJ und dann mit R2DBC). Zusätzlich wäre ein solches Set-up, wie bereits erwähnt, dank JDBC auch nicht reaktiv – eine Variante, die also sicherlich auch nur zum Ausprobieren geeignet ist, nicht aber für den Produktivbetrieb.

Fazit

Reactive Programming macht vor allem dann Sinn, wenn man große Datenmengen mit hoher Performance verarbeiten will. NoSQL-Datenbanken wie z. B. MongoDB bieten schon seit Längerem reaktive Schnittstellen; ist man aber an eine relationale Datenbank gebunden, sieht es mit der Unterstützung schon schlechter aus. Bisher gibt es kein standardisiertes API, um nicht-blockierend (und damit reactive) auf eine relationale Datenbank zuzugreifen.

Oracle plant das in Zukunft zu ändern und mit ADBA entsteht gerade ein Java-Standard, der genau dafür entwickelt wird. Wann dieser aber fertig ist und tatsächlich in Java einfließt, ist aktuell unklar. Bis dahin muss man auf anbieterspezifische Treiber zurückgreifen oder eine der verfügbaren Open-Source-Lösungen verwenden.

Wenn man sowieso mit dem Spring Framework unterwegs ist, bietet es sich dabei an, einen Blick auf R2DBC zu werfen. Diese Spring-Bibliothek bietet eine Integration von reaktivem Datenbankzugriff in die Spring Data Repositories und abstrahiert von verschiedenen Datenbankanbietern. So erhält man z. B. asynchronen Zugriff auf PostgreSQL und MS SQL über das Reactive API von Spring, dem Project Reactor. Zudem existiert bereits eine Anbindung an den aktuellen Stand von ADBA. R2DBC kann also als Aufsatz auf ADBA verwendet werden, sobald ADBA in Java eingeflossen ist. Bis dahin muss allerdings jedes Projekt für sich entscheiden, ob es den nativen asynchronen Treiber der jeweiligen Datenbank nimmt oder auf die Spring-Abstraktion R2DBC setzt.

In diesem Sinne: Stay tuned.

Geschrieben von
Arne Limburg
Arne Limburg
Arne Limburg ist Softwarearchitekt bei der open knowledge GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: