MicroStream 4 – High-Performance Java-Native Persistence

Native Anwendungen mit Java. Java-Services so schnell wie C. Startzeiten in Millisekunden und minimaler Memory-Footprint. Auf Basis von GraalVM entsteht ein neuer Java-Stack für moderne Cloud-Native-Applikationen, der Java auf ein völlig neues Level bringt. Mit MicroStream kommt jetzt eine hoch performante und gleichzeitig leichtgewichtige Persistence dazu die mit bis zu 1000 Mal schnelleren Datenbankzugriffen eine extrem hohe Performance bietet.

Mit GraalVM, Quarkus und leichtgewichtigen Microservice-Frameworks wie Micronaut und Helidon dringt Java in neue Dimensionen vor. Java-Anwendungen so schnell wie C-Programme, Startzeiten in Millisekunden, mit all den Stärken von Java – das klingt beeindruckend. Das ist der neue Java-Cloud-Native-Stack mit GraalVM im Kern. Er wird von Oracle stark vorangetrieben,, das von Oracle stark vorangetrieben wird um Java fit zu machen für die Entwicklung moderner, hoch performanter und resourcensparender Applikationen und Services für die Cloud. Das einzige was diesem Stack noch fehlt, ist eine ebenso leistungsfähige wie leichtgewichtige Persistence. Genau dafür wurde MicroStream entwickelt – seit kurzem in Version 4 mit bedeutenden Neuerungen verfügbar. Genau dafür wurde MicroStream entwickelt, das seit kurzem in Version 4 verfügbar ist und bedeutende Neuerungen mit sich bringt.

Paradigmenwechsel: Pure Java only

Genauso wie Microservices bei der Architektur oder Container bei der Virtualisierung zu einem Paradigmenwechsel führen Genauso wie Microservices zu einen Paradigmenwechsel bei der Architektur führen oder Container bei der Virtualisierung und damit ein Umdenken erfordern, setzt MicroStream einen Paradigmenwechsel und Umdenken bei der Datenspeicherung voraus. Gute Nachricht für Java-Entwickler: MicroStream verfolgt eine kompromisslose Pure-Java-Philosophie.

Java bietet alles für eine ultraschnelle Datenverarbeitung

Java ist die perfekte Technologie für hoch performante Datenverarbeitung und das JDK bietet alles was man dafür braucht, u.a. unter anderem eine universelle Datenstruktur, eine typsichere Abfragesprache, extrem hohe Performance zur Laufzeit, hohe Stabilität und Zuverlässigkeit.

Java-Objektgraphen – eine universelle Datenstruktur für jeden Anwendungsfall

In Java werden Daten in Form von Objekten bzw. komplexen Objektgraphen verwaltet, die sich hervorragend eignen um vernetzte, komplexe Daten zu abzubilden. Auf den ersten Blick erinnern Objektgraphen an das frühe hierarchische Datenbankmodell und bietet bieten ähnliche Vorteile, während es die gravierenden Nachteile von damals bei Objektgraphen nicht mehr gibt. Verknüpfungen über mehrere Ebenen sind mit Zirkelbezügen möglich und n:m Beziehungen sowie der Aufbau von Indizes, lassen sich mit Hilfe von Collections umsetzen. Ein Objektgraph kann sich aus Millionen von Objekten und tausenden Teilgraphen zusammensetzen. Man kann komplexeste Vernetzungen der Daten durch Zirkelbezüge abbilden, aber genauso gut einfache Listen oder gar nur ein einzelnes Objekt in einem Objektgraphen verwalten. Da alle Java-Typen sowie Collections verwendet werden können, kann man Java-Objektgraphen als eine Art Multi-Model-Datenstruktur bezeichnen.

Ultraschnelle Abfragen mit Java-Streams

Um Objektgraphen zu durchsuchen, bietet Java ab Version 8 mit der Java-Streams-API eine sehr effiziente Abfragesprache. Mit Parallel-Streams lassen sich die Suchvorgänge auf Objektgraphen zudem parallelisieren. Umso mehr Rechenkerne auf der Hardware-Ebene zur Verfügung stehen, desto schneller läuft die Suche. Das Durchsuchen komplexer Objektgraphen im RAM, selbst mit Datenmengen im Bereich mehrerer hundert Gigabyte, bewegt sich in der Regel im Bereich von nur wenigen Millisekunden. Bei, bei gut designten Objektmodellen zum Teil nur in Mikrosekunden. Mit Java-Objektgraphen und Streams API als Abfragesprache lassen sich zweifelsohne hoch performante Java-In-Memory-Datenbanken aufbauen. Das einzige was uns in Java seit jeher fehlt, ist eine Möglichkeit, Java-Objektgraphen effizient zu speichern. Genau dafür gibt es jetzt MicroStream.

Objektgraphen serialisieren – gute Idee nicht praxistauglich

Die Idee Objektgraphen direkt zu persistieren ist alles andere als neu. Mit Java 1.1 wurde Serialisierung eingeführt, um Objekte über ein Netzwerk zu übertragen oder in einer Datei persistieren zu können. Der größte Vorteil von Serialisierung ist, dass sie sehr einfach zu verwenden ist und keine spezielle Architektur voraussetzt – und es gibt nur ein einziges Datenmodell: Java-Klassen. Objekte müssen nicht mehr auf ein Datenbankmodell gemappt werden, sondern werden einfach in binärer Form gespeichert. Sämtliche mit Mapping verbundenen Probleme und Performanceverluste fallen vollständig weg. Die Vorteile klingen verlockend, sodass sich im Laufe der Jahre schon viele Entwickler mit diesem Ansatz auseinandergesetzt und ihre eigenen Erfahrungen damit gemacht haben.
Die Sache hat jedoch einen entscheidenden Haken. Die Java-Serialisierung ist stark limitiert, nicht sehr effizient und vorhersehbare Folgeanforderungen an Objektgraphpersistierung sind mit heutigen Serialisierungs-Frameworks nicht lösbar. Wird zum Beispiel ein Objekt serialisiert, das auf ein anderes Objekt referenziert, werden automatisch beide Objekte serialisiert. Das kann dazu führen, dass der gesamte Objektgraph, sprich die ganze Datenbank gespeichert wird – und umgekehrt, dass beim Deserialisieren automatisch die gesamte Datenbank in den RAM geladen wird. Ein weiteres großes Problem ist: , dass man beim Laden von Objekten erhält man Kopien dieser Objekte erhält, die keinen Bezug mehr zum Objektgraphen besitzen. Darüber hinaus wären viele weitere Probleme zu lösen. Deshalb sind bisher alle Ansätze, Objektgraphen mit Java-Serialisierung zu persistieren, mit dem Ziel OR-Mapping und relationale Datenbanken überflüssig zu machen, im Sande verlaufen.

Persistierung und dynamisches Laden von Objektgraphen und Teilgraphen

MicroStream ist nun der Durchbruch gelungen. Denn MicroStream ermöglicht es erstmals, einzelne Objekte eines Objektgraphen oder Teilgraphen persistent zu speichern. Umgekehrt lassen sich gezielt einzelne Objekte oder Teilgraphen nachladen, die dann automatisch in den Objektgraphen im RAM gemergt werden. Man muss sich nicht mehr mit Objektkopien, Session-Kontext und Objektlebenszyklen herumplagen. Um das zu ermöglichen, haben die MicroStream-Entwickler eine vollständig neue, ultraschnelle und gleichzeitig hochsichere Serialisierung entwickelt, die den Kern von MicroStream bildet. Die Performance von MicroStream-Applikationen ist förmlich atemberaubend. Datenbankabfragen werden im Vergleich zu ORM-Frameworks wie Hibernate bis zu 1000 Mal schneller ausgeführt. Das auf der MicroStream-Webseite laufende Performance-Demo (Abb. 1) ist auch auf GitHub veröffentlicht, sodass man damit selbst experimentieren kann. Damit ist MicroStream eine hoch effiziente Alternative zu JPA und ist geradezu prädestiniert für den Einsatz in Microservices mit eigener Persistenz. Die Objekte lassen sich wahlweise in relationale Datenbanken, NoSQL-Datenbanken, In-Memory-Datenbanken, In-Memory-Data-Grids, Distributed-Caches, Cloud-Blob-Stores oder in einfachen Files persistieren. Letzteres wird jedoch nur zu Testzwecken und Prototypen empfohlen.

Starten mit MicroStream

MicroStream ist eine kleine Java-Library, die man via Maven einbindet. Zur Laufzeit läuft MicroStream embedded in derselben JVM wie die Applikation.

(Listing 1)

Java-In-Memory-Datenbank

Ein Objektgraph im Hauptspeicher repräsentiert im MicroStream-Kontext eine Java In-Memory-Datenbank. Es ist wichtig zu verstehen, dass alle Datenbankoperationen auf dem Objektgraphen stattfinden und jede Änderung am Objektgraphen eine Änderung der Datenbank bedeutet.
Um einen persistierbaren Objektgraphen zu erzeugen, braucht man zuerst einen Startpunkt, eine sogenannte Root-Instanz. Alle Objekte, die gespeichert werden sollen, werden dann an die Root angehängt oder an Objekte, die bereits an der Root hängen.
Zu jeder Root-Instanz gehört immer eine Storage, in der die Daten in Form von Bytecode persistent gespeichert werden. Als Storage kann man eine relationale Datenbank, NoSQL Datenbanken, Cloud Blob-Stores, aber auch In-Memory-Datenbanken, In-Memory-Data-Grids, Distributed Caches, sogar Event-Streaming-Plattformen wie Kafka, aber auch einfache Dateien verwenden.

Elegantes Objektmodell mit POJOs

Definiert wird das Datenmodell ausschließlich durch Java-Klassen. Ein Datenbank-Ddatenmodell gibt es mit MicroStream nicht mehr und damit sind auch keine Mappings mehr nötig. Die Klassen sind reine POJOs. Es gibt weder eine Superklasse von der man ableiteten, noch Interfaces die man implementieren, oder Annotations mit denen man seine Klassen erweitern muss. Alle sinnvoll persistierbaren Java-Typen werden unterstützt. Auch mit Vererbung kann MicroStream umgehen. In Bezug auf die Struktur macht MicroStream nur eine einzige Vorgabe: Alle Daten, also die gesamte Datenbank, hängt an einer Root-Instanz. In einer Applikation kann es bei Bedarf auch mehrere Root-Instanzen geben. Der Aufbau des Objektgraphen kann völlig frei modelliert werden. Für ein gutes Objektmodell ist lediglich zu beachten, dass Objekte, auf die man häufiger zugreifen muss, möglichst nahe an der Root angelegt werden, sodass möglichst wenige Nodes durchlaufen werden müssen.

(Listing 2)

Objekte speichern

Um einen Objektgraphen zu persistieren, benötigt man nur noch einen Storage-Manager,, dem man die Root übergibt. Der Storage-Manager ist die Verbindung zur Storage.

(Listing 3)

Um bei einem Systemausfall keine Daten zu verlieren, sollten Objektgraph und Storage möglichst immer synchron sein. Das passiert jedoch nicht automatisch, sondern durch den Aufruf einer Store-Methode. Alles weitere erledigt MicroStream. Der Entwickler entscheidet also explizit darüber, ob und wann Änderungen am Objektgraphen persistiert werden. Lediglich eine einfache Regel ist dabei zu beachten: Es muss immer das Objekt gespeichert werden, das sich geändert hat.
Um ein im Objektgraphen neu hinzugefügtes Objekt zu speichern (Insert), wird nicht das neue Objekt selbst, sondern immer dessen Owner gespeichert, indem man die Store-Methode auf dem Owner aufruft. Kommt zum Beispiel bei einem Customer eine neue Order dazu, wird nicht die Order, sondern die Order-List gespeichert, in der die Order hinzugefügt wurde.

(Listing 4)

Möchte man dagegen ein im Objektgraphen geändertes Objekt speichern (Update), zum Beispiel eine bestehende Order die sich geändert hat, dann muss man das geänderte Objekt selbst speichern, indem man die Store-Methode direkt mit dem geänderten Order-Objekt aufruft.

(Listing 5)

Möchte man bestimmte Members nicht mit speichern, kann man diese als transient markieren. Bei jedem Store wird das zu speichernde Objekt in der Storage hinten angehängt (Append). Das gilt auch bei Updates. Jeder Store ist eine atomare All-or-nothing-Operation, vergleichbar mit einer Transaktion im relationalen Kontext. Wenn der Store erfolgreich war, ist garantiert, dass alle Daten in der Storage vorhanden sind (Strong-Consistency). Falls der Store nicht erfolgreich war, werden bereits persistierte Daten wieder gelöscht, was einem Rollback entspricht. Es ist zu beachten, dass dann auch auf dem Objektgraphen ein Rollback erfolgen muss, um den man sich jedoch im Moment noch selbst kümmern muss. Mit Hilfe von Channels lassen sich I/O Zugriffe parallelisieren.

Objekte löschen

Für das Löschen von Objekten ist keine spezielle Delete-Methode vorgesehen, denn dazu muss man wie in Java üblich, lediglich sämtliche Referenzen auf das zu löschende Objekt entfernen und diese Änderungen anschließend persistieren. Objekte, die nicht mehr referenziert werden, werden vom Garbage-Collector der Java-VM automatisch aus dem Speicher entfernt.

(Listing 6)

Objekte laden

Beim initialen Starten der Anwendung wird der gesamte Objektgraph im RAM erzeugt. Dazu werden jedoch lediglich die Objekt-IDs sowie alle Objekte die nicht als Lazy deklariert sind, direkt in den RAM geladen. Falls genügend Hauptspeicher verfügbar ist, spricht nichts dagegen, gleich die gesamte Datenbank in den RAM zu laden. Reicht der Hauptspeicher aber nicht aus, kann man sämtliche Objekte, die man erst später braucht, als Lazy definieren. Dadurch lädt die Engine sie erst dann in den RAM, wenn diese sie benötigt werden. MicroStream bietet ein sehr elegantes, vollständig objektorientiertes Programmiermodell. Man braucht keine aufwändigen Selects mehr, sondern kann mit gewöhnlichen Get-Methoden direkt auf die Objekte zugreifen, ohne sich darüber Gedanken machen zu müssen, ob sich die Objekte bereits im RAM befinden oder nicht. Liegt das gewünschte Objekt noch nicht im Speicher vor, wird es von der Engine automatisch geladen. Da I/O-Operationen auch mit MicroStream teuer sind, sollte man das Laden einzelner Objekte möglichst vermeiden und stattdessen lieber ganze Teilgraphen laden. Die geladenen Objekte werden automatisch in den Objektgraphen gemergt, sodass man sich nicht mehr mit Objekt-Kopien auseinandersetzen muss.

(Listing 7)

Objekte, die in den RAM geladen werden, brauchen deutlich mehr Speicher als in der Storage. Das liegt u.a. unter anderem an zusätzlichen Metadaten. Um Hauptspeicher zu sparen, kann man Lazy-Referenzen jederzeit auch wieder aus dem Hauptspeicher entfernen, sobald diese nicht mehr benötigt werden. Die Objekt-ID bleibt dabei im Objektgraphen erhalten, sodass das Objekt jederzeit erneut geladen werden kann. Um Ressourcen zu sparen, ist ein Vergleich der verschiedenen Java-VMs empfehlenswert. So verspricht die OpenJ9-JVM einen bis zu 63% geringeren Memory-Footprint und kürzere Startup-Zeiten. Sie und eignet sich damit sehr gut für Projekte mit MicroStream.

(Listing 8)

Was, wenn sich Versionen von Klassen ändern?

Sobald man eine Klasse ändert, indem man Attribute hinzufügt, entfernt oder ändert, passt die Klasse nicht mehr zu den bereits persistierten Objekten in der Storage. IDEs bieten dafür eine Refactoring-Funktion. Die gesamte Datenbank einem Refactoring zu unterziehen, wäre ggf.gegebenenfalls sehr zeitaufwändig. MicroStream ändert deshalb die persistenten Daten erst zur Laufzeit, nachdem sie zum ersten Mal geladen wurden. Dabei wird jeder veraltete (legacy) Typ auf einen entsprechenden neuen Typ gemappt. Jede Änderung muss abschließend neu persistiert werden. Die meisten Änderungen werden von einer Heuristik automatisch erkannt. Für komplexere Fälle kann man ein eigenes Type-Mapping definieren.

Garbage-Collection für die Storage

Damit die Storage nicht durch veraltete Objektversionen unendlich anwächst, werden diese von einem speziellen House-Keeping-Prozess, der wie ein Garbage-Collector auf File-Ebene funktioniert, automatisch aus der Storage entfernt. Dieser Prozess nimmt zudem permanent Optimierungen an den Daten in der Storage vor.

Zugriff von außen

Mit Hilfe einer REST-Schnittstelle können auch externe Prozesse auf die Storage zugreifen, z.B.um Beispiel für das Reporting. Auch eine graphische Benutzeroberfläche steht zur Verfügung. Mit Hilfe von GraphQL ist auch der Zugriff auf den Objektgraphen von außen möglich.

Läuft überall, wo eine JVM verfügbar ist

MicroStream läuft grundsätzlich überall dort, wo Java ab Version 8 läuft. Unter anderem auf dem Desktop, Server, in Container-Umgebungen und , auf Android. Es d, kann mit sämtlichen JVM-Sprachen wie Scala und Kotlin genutzt werden, ist prädestiniert für Microservices mit eigener Persistenz und läuft auch in, mit GraalVM oder Quarkus erzeugten, nativen Images. MicroStream ist in der Community Edition völlig lizenzkostenfrei verfügbar. Auch der kommerzielle Einsatz ist damit erlaubt.

Vorteile für Anwender

Es gibt drei Hauptargumente für den Einsatz von MicroStream:

1. Enorm hohe Performance und Datendurchsatz: Damit sind neue Innovationen, Features und Softwareprodukte möglich, die bislang auf Grund zu geringer Performance nicht denkbar waren. Anwender, die Probleme mit der Performance ihrer derzeitigen Systeme haben, sollten sich MicroStream unbedingt einmal ansehen.

2. Kostenersparnis in der Cloud oder On-Premis: Immer mehr Unternehmen verlagern ihre Systeme in die Cloud. Immer mehr Unternehmen verlieren den Überblick über ihre Systeme in der Cloud und die Kosten laufen zum Teil völlig aus dem Ruder. Insbesondere in der Post-Corona Zeit müssen Unternehmen Kosten reduzieren und ihre Anwendungen kosteneffizient entwickeln und betreiben. Durch die hohe Performance von MicroStream brauchen Anwendungen nur noch einen Bruchteil an Rechenleistung. Matthias Wenzl, Geschäftsführer der Consulting-Firma Caweco und Implementierungspartner des Versicherungskonzerns Allianz, sagt: „Hätten wir unsere Anwendungen wie früher auf Basis von JPA entwickelt, hätten wir teure Cluster-Architekturen betreiben müssen. Mit MicroStream laufen heute die meisten unserer Anwendungen containerisiert auf kleinen bis mittleren virtuellen Server-Instanzen. Wir sprechen von Einsparungen von bis zu 90% Infrastrukturkosten jährlich.“

3. Hoher Entwicklungskomfort: MicroStream verfolgt eine Pure-Java-Philosophie. Das Programmiermodell ist sehr elegant und vollständig objektorientiert. Entwickler müssen ihre Klassen nicht mehr an eine Datenbank anpassen und die Komplexität der Anwendungsarchitektur wird drastisch reduziert. Der gesamte Datenbankentwicklungsprozess wird vereinfacht und beschleunigt.

Wo ist der Haken?

MicroStream richtet sich gezielt an Java-Entwickler. Gute Kenntnisse in OOP, Design von Objektmodellen sowie Concurrency-Programmierung werden vorausgesetzt. Leider gibt es bislang noch keine SQL-Schnittstelle, über die externe Systeme auf die Storage zugreifen können. Eine solche wurde aber bereits angekündigt. Der Zugriff von außen ist via REST und GraphQL jetzt schon möglich.
MicroStream bildet das Objektmodell nicht mehr auf ein Datenbankmodell ab, sondern persistiert die serialisierten Daten als Binaries direkt in der Datenbank. Datenbankspezifische Funktionen bleiben damit weitestgehend ungenutzt. Datenbankverfechter werden dem Pure-Java-Konzept eher skeptisch begegnen. Durch die Datenbankabstraktion mit JPA hat man in Java aber ohnehin schon diesen Weg eingeschlagen. Ein Großteil an möglichen Datenbankfunktionen bleibt bereits mit JPA außen vor, unter anderem Stored-Procedures, Stored-Functions, PL/SQL etc. Während Datenbankhersteller propagieren, Businesslogik direkt in der Datenbank auszuführen, wird mit JPA die Businesslogik in den Application-Layer verlegt und für uns selbstverständlich in Java implementiert. Nur ein wenig Businesslogik gestatten wir in der Datenbank zu laufen, z.B. Concurrency-Management. Genau das bereitet uns dann oft Probleme und wir müssen uns am Ende des Tages trotzdem wieder selbst darum kümmern. MicroStream geht diesen Weg nun konsequent zu Ende, indem man die Datenbank ausschließlich als reine Daten-Storage betrachtet und die Businesslogik vollständig in den Java-Server verlegt. Dieser Paradigmenwechsel muss verstanden werden, um die Vorteile dieses Konzepts erfassen zu können.

Fazit:

MicroStream ist prädestiniert für Microservices, die ihre eigene Persistenz benötigen. Aber auch in großen monolithischen Anwendungen hat sich MicroStream als Alternative zu JPA bereits bewährt. Damit ist das Einsatzfeld enorm breit. Es gibt bereits zahlreiche Applikationen im Umfeld von ERP- und Branchenlösungen, meist individuelle Zusatzmodule, die ebenfalls eine eigene Persistence benötigen. Auch für Anwender von Graph-Datenbanken kann MicroStream eine sehr interessante Alternative sein.

Wer erst einmal etwas Praxiserfahrung sammeln möchte, kann MicroStream überall dort einsetzen, wo heute zum Beispiel H2-Datenbanken eingesetzt werden, unter anderem für Unit-Tests und die Entwicklung von Prototypen. Wiederum prädestiniert ist MicroStream als Persistence für Android-Apps. Hierfür bietet MicroStream einen Connector zu SQLite.

MicroStream ist als Community-Edition frei verfügbar unter www.microstream.one.

Richard Fichtner

Richard Fichtner ist ein passionierter Java Entwickler mit mehr als 15 Jahren Erfahrung in der Softwarebranche. Er engagiert sich in der Open Source Community, um das Wissen über Java-Technologien zu verbreiten. Er spricht auf Konferenzen wie Oracle Code / JavaOne / JCON / Clean Code Days und vielen anderen und leistet einen Beitrag zu verschiedenen Open Source Projekten wie http://rapidclipse.com/. Als Oracle Groundbreaker Ambassador Alumni und AWS Certified Solution Architect unterstützt er Teams beim Einsatz von Cloud Lösungen. Seine Interessenschwerpunkte sind Clean-Code, Cloud, neue Technologien und alles was agil ist. In seiner Freizeit organisert er die Java User Group Oberpfalz. 

 

Redaktion


Leave a Reply