dark

MicroStream: In-Memory Datenbanken mit Pure Java

Avatar

#JAVAPRO #JCON2019 #InMemoryDatabase #NoSQL

Mit MicroStream gibt es jetzt einen neuen Ansatz für die Persistierung von Daten in Java. MicroStream speichert Java-Objektgraphen genauso, wie diese im RAM existieren. Objekte müssen nicht durch Annotation oder XML-Konfigurationen aufwändig auf eine künstliche Struktur gemappt werden. Das In-Memory-Konzept von MicroStream ermöglicht Datenzugriffe im Bereich von Nanosekunden – bis zu 100.000 Mal schneller als Festplattenzugriffe relationaler Datenbanken.

Die Entwicklung von Datenbankanwendungen in Java gemäß dem Java-Standard JPA (Java-Persistence-API) ist ein komplexes Thema. Wer Datenbank-Applikationen für den professionellen Einsatz mit JPA entwickeln möchte, muss erfahrungsgemäß eine Menge Erfahrung im Umgang mit ORM-Frameworks wie Hibernate besitzen. Zudem erhöht sich die Komplexität einer Applikation mit dem Einsatz von JPA sprunghaft. Ganz besonders fällt das bei der Entwicklung von Microservices auf. Sobald man JPA verwenden möchte um Daten zu persistieren, explodiert der zuvor noch schlanke Programm-Code förmlich. Auch für mobile Geräte ist JPA zu schwergewichtig.

Mit MicroStream gibt es jetzt einen völlig neuen Ansatz für die Persistierung von Daten in Java, der

  • JPA überflüssig macht,
  • In-Memory Datenzugriffe ermöglicht, die im Bereich von Nanosekunden liegen – bis zu 100.000 Mal schneller als Festplattenzugriffe wie bei relationalen Datenbanken häufig der Fall,
  • die gesamte Datenbankentwicklung mit Java stark vereinfacht und beschleunigt.

 

RDBMS und Java sind völlig inkompatibel

Java stellt Daten bekanntlich in Form von Objekten, oder genauer in Form von Objektgraphen, dar. Dagegen sind fast alle großen Datenbanksysteme wie MySQL relationale Datenbanken (RDBMS). Die Daten werden hier in zweidimensionalen Tabellen gespeichert, die über Relationen miteinander verknüpft sind. Java und RDBMS sind demnach in Bezug auf die verwendete Datenstruktur völlig inkompatibel. Dieses Problem ist als Object-relational-impedance-mismatch[1] bekannt. Dazu gibt es weitere Probleme:

  • Objektidentität – Objekte lassen sich eindeutig identifizieren (OID), Datensätze müssen dazu um einen künstlichen Primärschlüssel erweitert werden.
  • Vererbung und Verhalten – Im relationalen Paradigma gibt es keinen vergleichbaren Ansatz.
  • Datenkapselung – In Objekten wird der der direkte Zugriff auf Attributwerte verhindert, in RDBMS lassen sich Datensätze direkt ändern.
  • Skalierung – RDBMS skalieren auf Grund ihrer Datenstruktur vertikal sehr gut, horizontal dagegen nicht gut.
  • Zugriff auf Daten – In Java greift man objektorientiert, d.h. mit Hilfe von Methoden auf Daten zu, auf RDBMS dagegen mit SQL. Als Abfrageergebnis erhält man kein Objekt, sondern ein JDBC-Resultset, über das man iterieren muss, um an die Daten zu gelangen.

JPA sollte die Datenbankentwicklung mit Java standardisieren und vereinfachen

Um objektorientiert auf relationale Datenbanken zugreifen zu können, wurden sogenannte ORM-Frameworks (Objekt-Relationales-Mapping) wie Hibernate entwickelt. Hibernate bildet eine Schicht zwischen der Java-Anwendung und der Datenbank, welche die darunterliegende Datenbank abstrahiert. Um das Mapping zwischen Objekten und Tabellen kümmert sich das Framework vollautomatisch. Im Hintergrund muss es jedoch eine Menge leisten, insbesondere:

  • für jeden Datenbankzugriff entsprechende SQL-Statements generieren und absetzen,
  • Java-Typen auf (häufig proprietäre) Datenbankdatentypen mappen (z.B. String auf VARCHAR) und umgekehrt,
  • aus JDBC-Resultsets entsprechende Java-Objekte erzeugen und vieles mehr.

Vor 10 Jahren wurde der objektrelationale Ansatz im JPA-Standard (Java-Persistence-API) standardisiert (JSR 220 [2]). Die Referenzimplementierung dazu war TopLink, dem Vorläufer des heutigen EclipseLink.

 

JPA ist komplex und erhöht den Entwicklungsaufwand drastisch

Die enorme Komplexität von JPA ist eine große Herausforderung für Entwickler. Für die Kombination von Java und RDBMS müssen immer zwei Datenmodelle entwickelt und bei Änderungen entsprechend angepasst werden. Das ist nicht nur doppelter Aufwand, sondern stellt damit auch eine potentielle Fehlerquelle dar. IDEs bieten zwar gute Tools, insbesondere für die Entwicklung mit Hibernate, trotzdem muss der Entwickler sehr viel manuell konfigurieren und später generierten Code nochmals händisch optimieren, was wiederum sehr fehleranfällig ist. Das Java Entity-Klassenmodell kann nicht mehr völlig frei modelliert werden, sondern muss an das relationale Tabellenmodell angepasst werden. Für das ORM-Framework müssen die Entity-Klassen annotiert werden oder es bedarf einer XML-Konfigurationsdatei. Für die Abbildung von Vererbungsbeziehungen sind zusätzliche Tabellen nötig.

Ans Eingemachte geht es spätestens dann, wenn es um die Persistierung von Objekten selbst geht. Denn persistieren lassen sich nur Objekte, die an einen einen speziellen EntityManager gebunden sind (attached). Objekte, die dagegen detached sind, werden als normale Java-Objekte betrachtet und nicht mit der Datenbank synchronisiert. Auch bei der Persistierung selbst kann man sehr Fehler machen, u.a. bei der Wahl der richtigen Methode (flush, persist oder merge).

Ein großer Nachteil von JPA ist sicher die Performance. Permanentes Generieren von SQL-Statements sowie OR-Mapping kostet eine Menge Rechenzeit und ist damit teuer. In den meisten Anwendungsfällen wird deshalb ein zusätzlicher Data-Cache verwendet, um den hohen Performanceverlust zu kompensieren. Ohne den sogenannten Second-Level-Cache (z.B. EHCache) wäre Hibernate in vielen Anwendungen praktisch kaum brauchbar. Deshalb sind JPA-Entwickler häufig mit Performance-Optimierungen durch Konfigurationsänderungen beschäftigt. Doch auch Caching ist komplex und gilt als eines der am meisten missverstandenen Konzepte bei der Datenbankentwicklung in Java.

Auch wenn es um Queries geht, wird mit JPA alles komplizierter. Zwar lassen sich mit JPA auch sogenannte native Queries in Form von SQL-Strings absetzen, aber dies hat gravierende Nachteile, u.a. sind diese nicht typsicher, es gibt keinerlei IDE-Unterstützung wie Refactoring, Compiler-Checks, Quickfixes oder ähnliches – und der SQL-Code ist dann meist datenbankspezifisch. Deshalb stellt JPA mit JPQL und der JPA-Criteria API spezielle Query-APIs zur Verfügung, mit denen man sämtliche Abfragen in Java schreiben kann. Während die eine (JPQL) ebenfalls Plain-Strings enthält und damit dieselben Nachteile wie native SQLs hat, ist die andere (Criteria) kompliziert und der Code wirkt aufgebläht.

Die Liste an Problemen und Herausforderungen ließe sich noch weiterführen. Mit den über JPA und insbesondere Hibernate verfassten Abhandlungen und Bücher ließe sich problemlos ein Wandregal füllen, was allein schon ein Indiz dafür ist, dass man es hier nicht mit einem trivialen Thema zu tun hat. Jeder, der etwas tiefer in die Thematik einsteigt, muss sich zwangsläufig fragen: „Ich möchte doch eigentlich nur Daten speichern. Geht das mit Java nicht auch einfacher?“

NoSQL – die bessere Datenbanktechnologie?

Moderne NoSQL Datenbanken versprechen schneller und einfacher zu sein, was sie natürlich auch für Java Entwickler interessant macht. Grundsätzlich rechnet man alle nicht-relationalen Datenbanken dem NoSQL Segment zu. Man unterscheidet grob zwischen dokumentenorientierten-, spaltenorientierten-, Key-Value-, Graph- und älteren Objektdatenbanken. Die Hersteller positionieren sich meist gezielt, indem sie sich auf bestimmte Anwendungsfälle spezialisieren. Der wohl größte Vorteil von NoSQL Datenbanken ist, das ihre Datenstrukturen besonders gut geeignet sind, um horizontal zu skalieren, was sie vor allem für den Einsatz in der Cloud interresant macht.

Das wohl größte Problem bei NoSQL sind, dass man umlernen und proprietäre Abfragesprachen lernen muss und, dass man wie mit RDBMS ebenfalls ein zweites Datenmodell erstellen und pflegen muss. Damit benötigt man wie auch bei relationalen Datenbanken ein Mapping und muss seine Java-Anwendung speziell für die jeweilige Datenbank anpassen. Nur ist hier ein Datenbankwechsel aufgrund des fehlenden Standards wie JPA noch sehr viel problematischer als bei RDBMS. Ob sich der Umstieg rentiert, hängt vom jeweiligen Anwendungsfall ab.

Java-Objekte persistieren ohne Mapping

Mit MicroStream gibt es jetzt einen völlig neuen Ansatz für die Persistierung von Daten in Java. MicroStream ist eine in Java geschriebene Storage-Engine für die Speicherung nativer Java Objekt-Graphen. Mit MicroStream lassen sich beliebige Java Objekt-Graphen (POJO) aus dem Hauptspeicher auslesen, persistent speichern und ganz oder teilweise zurück in den Hauptspeicher laden.

An dieser Stelle ist wichtig zu erwähnen, dass MicroStream nicht mit früheren Objektdatenbanken oder heutigen Graph-Datenbank zu verwechesln ist. Auch Objekt- und Graph-Datenbanken verwenden ihre eigene Objekt- bzw. Graph-Struktur, während MicroStream Java Objekt-Graphen nativ speichert, d.h. unverändert.

Einbinden von MicroStream

MicroStream ist eine nur wenige MByte kleine Java-API, die man in jede Java-Anwendung via Maven einbinden kann:

Listing (1) – Maven repositories
<repositories>
<repository>
<id>microstream-releases</id>
<url>https://repo.microstream.one/repository/maven-public/</url>
</repository>
</repositories>
Listing (2) – Maven dependency
<dependency>
<groupId>one.microstream</groupId>
<artifactId>ARTIFACT_ID</artifactId>
<version>02.00.00-MS-GA</version>
</dependency>

Architektur von MicroStream

Der größte Unterschied zu herkömmlichen Datenbanksystemen (und speziell zu Graph- und Objektdatenbanken) besteht darin, dass MicroStream kein autark laufender Datenbank-Server, sondern eine reine Storage-Engine ist – ähnlich wie InnoDB für MySQL. Jetstream selbst ist mit 2,5 MB eine vergleichsweise winzige Java-API, die man wie jede andere API als JAR direkt in seine Java-Anwendung einbindet. D.h., Jetstream ist ein fester Teil der Anwendung und läuft (embedded) zusammen mit der Anwendung in derselben Laufzeitumgebung. Die eigentliche Daten-Storage ist eine einfache Textdatei, die sich an einem beliebigen Ort befinden kann.

Funktionsprinzip

Eine MicroStream-Datenbank besteht jeweils aus einem (einzigen) Java-Objektgraphen, an dem alle Daten hängen, ähnlich einem Tree. Das oberste Element wird als Root bezeichnet. Zur Laufzeit wird also zuerst eine Root-Instanz erzeugt.

Listing (3) – Instanziierung
public class DataRoot
{
private String content;

public DataRoot()
{
super();
}

public String getContent()
{
return this.content;
}

public void setContent(final String content)
{
this.content = content;
}

@Override
public String toString()
{
return "Root: " + this.content;
}
}
}

Anschließend lassen sich die Datenobjekte anhängen. MicroStream kommt dabei mit allen gängigen Java-Typen zurecht. Auch Collections können verwendet werden. Der gesamte Objektgraph befindet sich dabei im Hauptspeicher.

Listing (4) – Beliebige Klassen persistierbar, z.B: Customer
public class Customer {

private String firstname;
private String lastname;
private String mail;
private Integer age;
private Boolean active;

...
}
Listing (5) – Customers in der Klasse RootData einbinden
public class DataRoot
{
private String content;

public DataRoot()
{
super();
}

public String getContent()
{
return this.content;
}

public void setContent(final String content)
{
this.content = content;
}

@Override
public String toString()
{
return "Root: " + this.content;
}
}
}

Persistierung

Um ein Objekt zu speichern, muss immer auf dessen Parent die Store-Methode aufgerufen werden. Die für die Persistierung nötigen Objektinformationen werden dann aus dem RAM ausgelesen und (anhängend) in die File-Storage geschrieben. Der gesamte Vorgang ist transaktionssicher.

Listing (6) – Neuen Customer persistieren
final Customer customer = new Customer();

customer.setFirstname(„John“);
customer.setLastname(„Travolta“);
customer.setMail(„john.travolta@gamil.xy“);
customer.setAge(63);
customer.setActive(true);

</pre>
DataRoot root = microstreamDemo.root();
root.getCustomers().add(customer);

microstreamDemo.store(root.getCustomers());

JPA fällt vollständig weg

Da MicroStream anders als Datenbanken keine eigene Datenstruktur vorgibt, sondern Java Objekt-Graphen unverändert persistiert, ist mit MicroStream kein Mapping mehr nötig. JPA ist damit überflüssig. Ohne permanentes OR-Mapping ergibt sich ein wahrer Performance-Boost.

Daten löschen

Für das Löschen von Daten gibt es in MicroStream keine spezielle Methode. Objekte die vom Objekt-Graphen nicht mehr erreichbar sind, werden automatisch vom Grabage-Collector der Java VM aus dem Speicher gelöscht. Nach der Persistierung (des Partent-Objektes) wird das im RAM gelöschte Objekt einfach nicht mehr in die Storage geschrieben. Beim späteren Laden verwendet  MicroStream immer den letzten Versionsstand eines Objektes in der Storage. Damit sich in der Storage nicht zu viel Datenmüll ansammelt und unnötig viel Speicher verbraucht, besitzt MicroStream einen eigenen Garbage-Collector Prozess, der auf der Storage operiert und diese in einem konfigurierbarem Thread laufen permanent von Datenmüll befreit.

In-Memory Konzept

Beim Anwendungsstart wird der gesamte Objektgraph initial im Memory erzeugt. Dabei werden zunächst nur die Objekt-IDs in den Speicher geladen. Erst wenn der Zugriff auf ein bestimmtes Objekt erfolgt, werden dessen Daten automatisch in den RAM geladen. Falls man ausreichend Hauptspeicher zur Verfügung hat, kann man die gesamte Datenbank in den RAM laden und man erhält damit eine vollständige In-Memory Datenbank. Falls die Datenbank dafür jedoch zu groß ist, lädt man bestimmte Objektreferenzen einfach erst dann in den Speicher, wenn man diese braucht. Dafür bietet MicroStream Lazy-Loading, das ähnlich wie bei JPA funktioniert.

Keine Abfragesprache nötig

Mit MicroStream repräsentiert der Objektgraph im Hauptspeicher die Datenbank. Mit Hilfe der Java 8 Streams API lassen sich Objektgraphen im Speicher sehr effizient durchsuchen, sodass MicroStream keine eigene Abfragesprache braucht.

Listing (7) – Suchen
</pre>
public static void booksByAuthor()
{
final Map<Author, List<Book>> booksByAuthor =
ReadMeCorp.data().books().stream()
.collect(groupingBy(book -> book.author()));

booksByAuthor.entrySet().forEach(e -> {
System.out.println(e.getKey().name());
e.getValue().forEach(book -> {
System.out.print('\t');
System.out.println(book.title());
});
});
}
<pre>

Abfragen 100.000 Mal schneller als herkömmliche Datenbanken?

Bei konventionellen RDBMS befindet sich die Datenbank auf der Festplatte. Um SQL-Queries abzuarbeiten, müssen RDBMS damit permanent auf die Festplatte zugreifen. Die mittleren Zugriffszeiten auf HDDs liegen bekanntlich im Bereich von etwa 9 Millisekunden, was umgerechnet 9 Millionen Nanosekunden entspricht. Zugriffe auf SDRAM dauern dagegen im Durchschnitt nur nur 65 Nanosekunden und sind damit rund 100.000 Mal schneller als Festplattenzugriffe. Selbst sehr komplexe Abfragen auf große Objektgraphen werden i.d.R. im Bereich von Mikrosekunden abgearbeitet und sind damit immer noch um ein Vielfaches schneller als konventionelle Datenbanken. Diese ohnehin schon astronomisch anmutenden Werte in der Praxis tatsächlich erreicht werden, dafür sorgt der JIT-Compiler der Java VM, der Streams-Operationen nochmals um den Faktor 5 bis 10 beschleunigt. Mit dem Einsatz von Parallel-Streams, die parallel ablaufende Abfragen auf einem Objektgraphen ermöglichen, sowie mit dem Einsatz speicheroptimierter JVMs wie OpenJ9, gibt es zudem noch weitere Möglichkeiten um die Performance noch weiter zu steigern.

Kein Netzwerk-Flaschenhals mehr

Da die Daten einer MicroStream Datenbank direkt im Hauptspeicher des Java Anwendungs-Servers liegen, müssen diese nicht mehr über das Netzwerk transportiert werden. Der berüchtigte Netzwerk-Flaschenhals, der einer konventionellen Architektur nochmals enorm viel Performance raubt, fällt damit weg.

Einspruch! Data-Caches und In-Memory Datenbanken sind genauso schnell!

Es ist mittlerweile nicht nur allgemein bekannt, sondern auch akzeptiert, dass konventionelle, auf Festplatten operierende RDBMS viel zu langsam für große Anwendungen sind. Deshalb kommt mittlerweile in den meisten Anwendungsfällen ein zusätzlicher Data-Cache zum einsatz, der Abfrageergebnisse im schnellen Hauptspeicher vorhält, sodass auf diese Weise sämtliche Abfrageergebnisse nicht mehr von der langsamen Festplatte, sondern ebenfalls direkt aus dem schnellen Hauptspeicher gelesen werden können. Eine weitere Option könnte der Einsatz einer In-Memory Datenbank sein.

Sowohl In-Memory Datenbank als auch die Kombination aus RDBMS und Data-Cache können die Performance einer Pure Java Application, die durch die JVM vollautomatisch performanceoptimiert wird, kaum erreichen, da in beiden Fällen nach jedem Lesezugriff ein rechenzeitintensives OR-Mapping durchgeführt werden muss, das beim Pure-Java-Ansatz nicht nötig ist, da immer ein- und derselbe bereits bestehende Objekgraph nur upgedated wird.

Das Java Entity-Klassenmodell ist das einzige Datenmodell

Mit MicroStream gibt es nur noch ein einziges Datenmodell: das Java Entity-Klassenmodell. Anders als bei JPA müssen die Entity-Klassen mit MicroStream nicht annotiert werden. MicroStream kommt mit allen gängigen POJOs zurecht. Der Entwickler kann damit sein Klassenmodell völlig individuell designen und muss keinerlei Rücksicht mehr auf die Persistenzschicht nehmen. Nötige Änderungen und Erweiterungen, die man am Datenmodell vornehmen muss, sind völlig unproblematisch.

Wo ist der Datenbank-Server?

Eine Datenbank bietet heutzutage u.a. eine Benutzerverwaltung, Import-/Export-Schnittstellen, kümmert sich um Nebenläufigkeiten (Sessions, Connections, Caching), ermöglicht das Auslagern von Business-Logik (Stored Procedures, Trigger, Constraints) und natürlich die Daten-Storage. Im Zeitalter zweischichtiger-Architekturen waren diese Funktionen essentiell. In modernen 3-Tier-Architekturen werden die meisten dieser Funktionen vom Anwendungs-Server übernommen, weil man diese auf Grund der meist sehr individuellen Anforderungen lieber selbst in Java implementieren wird oder muss. D.h., dass in Java werden die meisten Features die Datenbank-Server zur Verfügung stellen, gar nicht verwendet. In vielen Projekte wird die Verwendung der nativen Datenbankfunktionen jedoch vorgegeben, was dann Komplexität und Entwicklungsaufwand unnötig erhöht. Genau genommen gibt es nur eine Funktion im Datenbank-Server, die man in dringend Java braucht: die Storage-Engine. Alles andere wird für gewöhnlich ohnehin in Java implementiert. Das ist der Grund, warum sich MicroStream bewusst auf die reine Daten-Storage, das Speichern von Objektgraphen, begrenzt und kein weiterer Datenbank-Server sein will.

Migration auf MicroStream

Bereits bestehende JPA-Projekte lassen sich recht leicht auf MicroStream umstellen. Das JPA-Entity-Klassenmodell kann man unverändert beibehalten. Die vorhandenen JPA-Annotationen sind unschädlich und können auch irgendwann später entfernt werden. Um die Daten zu migrieren, müssen diese lediglich einmalig in den Hauptspeicher geladen und anschließend mit MicroStream persistiert werden. Möchte man eine relationale Datenbank auf MicroStream portieren, für die noch keine Java-Klassen vorhanden sind, kann man sich diese mit Hilfe der JBoss Hibernate-Tools sämtliche Entity-Klassen durch Import generieren lassen.

Fazit:

MicroStream ist eine Data-Persistence-Engine, mit der sich native Java-Objektgraphen völlig ohne ein Mapping persistieren lassen. JPA wird damit völlig überflüssig. Der Entwickler muss nur noch ein einziges Datenmodell erstellen und pflegen: ein Java Entity-Klassenmodell. Damit vereinfacht MicroStream die Datenbankentwicklung in Java enorm. Mit MicroStream repräsentiert der Java-Objektgraph im Hauptspeicher die Datenbank. Ist genügend Hauptspeicher vorhanden, kann man die gesamte Datenbank in den RAM laden und man erhält damit eine In-Memory Datenbank. Ist die Datenmenge zu groß, lädt man mit Lazy-Loading einfach nur eine kleinere Menge an Daten in den Speicher. Abfragen werden direkt in Java formuliert, z.B. mit Java 8 Streams. Das ermöglicht enorme Zugriffsgeschwindigkeiten im Bereich von Mikrosekundenn. Abfragen sind damit bis zu 100.000 Mal schneller als bei konventionellen Datenbanken. MicroStream ist eine vergleichsweise kleine Java API, die man vie Maven herunterladen und in jede beliebige Java-Anwendung einbinden kann. MicroStream ist unter www.microstream.one verfügbar und auch für den kommerziellen Einsatz völlig lizenzkostenfrei.


[well]

Autor – Sebastian Späth


Sebastian Späth ist Senior Java Developer und Technology Evangelist bei der XDEV Software Corperation.
Er verfügt über eine breite Berufserfahrung und war mit Rapiclipse an der Entwicklung einer eigenen Eclipse-Distribution beteiligt. Sebastians Schwerpunkte sind Rapid Devolpement und Rapid Prototyping.
LinkedINXING

[/well]

 

Total
0
Shares
Previous Post

Was DevOps heute wissen müssen

Next Post

Nachhaltige Report-Entwicklung mit TDD

Related Posts