Innovation

Dokumente, Datenbanken und Eventual Consistency

Dino Esposito

Dino EspositoAngenommen, Sie hätten die letzten paar Jahre in einer Höhle ohne Internetverbindung festgesessen. Und nun, nach Ihrer Rückkehr in die Zivilisation, wird Ihnen ein brandneues Projekt zugewiesen. Bei der ersten technischen Besprechung verfolgen Sie die angeregte Diskussion der Teammitglieder, die sehr kontroverse Ansichten zu Dokumentdatenbanken vertreten.

Was ist eigentlich eine Dokumentdatenbank? Wir haben Daten doch sonst in einer ganz normalen Instanz einer relationalen SQL Server-Datenbank gespeichert. Welche Vorteile bieten nun Dokumentdatenbanken? Sie hören sich die unterschiedlichen Meinungen an und versuchen, sich einen Reim darauf zu machen. Sie fragen sich, anhand welcher Kriterien Sie eine fundierte Entscheidung dazu treffen können, ob bei Ihrem nächsten Projekt Dokumentdatenbanken anstelle von relationalem Speicher zum Einsatz kommen sollten.

In diesem Artikel geht es nicht darum, in Bezug auf Dokumentdatenbanken und NoSQL-Speicher im Allgemeinen Position zu beziehen. Ich möchte jedoch die Perspektive eines skeptischen Benutzers einnehmen, der zwar gerne neue Möglichkeiten auslotet, um Dinge zu optimieren, und stets auf der Suche nach konkreten und messbaren Vorteilen ist, aber eben nicht um jeden Preis.

SQL und NoSQL

Das relationale Modell und die Structured Query Language (SQL) gibt es nun schon seit mehr als 40 Jahren. Das ist eine unglaublich lange Zeit in dieser sich rasant ändernden Branche. Im Lauf der letzten Jahrzehnte hat sich das relationale Modell gegen objektorientierte Datenbanken durchgesetzt, die dem altmodischen relationalen Modell mit dem Aufkommen der objektorientierten Sprachen und Modellierungsmuster zunächst den Rang abzulaufen schienen.

Dazu kam es jedoch nicht. Stattdessen tauchten nun die ORM-Tools (Object-Relational Mapping) auf. Im .NET-Bereich entwickelte sich zunächst NHibernate zum Standard, der kürzlich vom Entity Framework abgelöst wurde. Darüber hinaus gab es weitere ähnliche kommerzielle und Open-Source-Frameworks unterschiedlicher Anbieter und Entwickler. Es lässt sich also schon einmal festhalten, dass die Objektmodellierung noch kein auseichender Grund ist, das relationale Modell aufs Abstellgleis zu schieben.

Dennoch setzen immer mehr Unternehmen auf NoSQL-Speicher. Da lohnt es sich nachzuhaken, in welchen Bereichen die NoSQL-Speicher eigentlich zum Einsatz kommen. Dies ist sehr viel interessanter als die Frage, wie sich von NoSQL-Speichern profitieren lässt. Tatsächliche Anwendungsfälle mit den eigenen Voraussetzungen zu vergleichen, scheint sehr viel sinnvoller, als auf der Suche nach Gründen für die Verwendung einer bestimmten Technologie im Dunkeln zu stochern. Beim genaueren Blick auf die Bereiche, in denen NoSQL-Speicher verwendet werden, kristallisieren sich die folgenden Aspekte heraus:

  • Groß – häufig unberechenbar große Datenmengen und Millionen von Benutzern
  • Tausende von Abfragen pro Sekunde
  • Unstrukturierte/Teilstrukturierte Daten, die in unterschiedlicher Form vorliegen, aber gleich behandelt werden müssen (polymorphe Daten)
  • Cloud Computing und virtuelle Hardware, um eine hochgradige Skalierbarkeit zu ermöglichen

Wenn sich keine dieser Voraussetzungen mit den Anforderungen Ihres Projekts deckt, wird sich für Sie der Einsatz von NoSQL-Technologie höchstwahrscheinlich nicht lohnen. In diesem Fall hieße das, einfach dieselben Dinge auf andere Weise zu tun.

Unterschiede in der Struktur

NoSQL bietet sich an, wenn hochgradig interaktive Anwendungen erstellt werden müssen, durch die eine große Zahl an Schreib- und Lesevorgängen an den Datenbankserver übertragen wird. In einem relationalen Modell funktionieren die Beziehungen zwischen Tabellen hervorragend, so lange die betreffenden Tabellen festgelegte Schemas aufweisen. Diese Tabellen müssen das Domänenmodell originalgetreu und realistisch widerspiegeln.

Wenn Ihre Daten – unabhängig von Umfang und Zugriffshäufigkeit – gut in „strukturierte Tabellen“ passen, dann ist SQL Server bestens für Ihre Zwecke geeignet. Die bewährte Technologie hat noch lange nicht ausgedient, sondern wird im Lauf der Zeit immer besser. Die Columnstore-Funktion von SQL Server 2014 bietet Ihnen Unterstützung, wenn Sie es mit einer großen Anzahl an Spalten zu tun haben. Eine große Anzahl an Spalten kann zu einer Beeinträchtigung der Abfragegeschwindigkeit führen. Eine derartige Menge an Spalten entsteht unter Umständen auch bei dem Versuch, einem relationalem Modell teilstrukturierte Daten zuzuordnen.

Ein Columnstore (spaltenbasierter Datenspeicher) ist eine einfache Tabelle mit den üblichen Zeilen und Spalten, deren Inhalt jedoch physisch nach Spalten angeordnet und gespeichert wird. Entsprechend werden alle Daten in einer beliebigen Spalte auf derselben physischen SQL-Seite gespeichert. Wenn Sie alle Daten aus einer großen Anzahl ausgewählter Spalten abfragen müssen, können Sie mit diesem neuartigen Format die Abfrageleistung maßgeblich steigern, ohne die Persistenzebene neu entwerfen zu müssen.

Immer mehr Anwendungen liegen heute komplexere Datenmodelle zugrunde, für die strukturierte Tabellen im Allgemeinen zu groß sind. Daher fragen sich Anwendungsarchitekten, ob wirklich Behelfs- und Kompromisslösungen nötig sind, um derartige teilstrukturierte Daten innerhalb der Grenzen von starren SQL-Schemas zu speichern und abzufragen. Wenn Sie Ihre Daten erfolgreich zu einem relationalen Schema modellieren können, werden Sie wahrscheinlich Joinanweisungen für eine große Anzahl an allgemeinen Abfragen in Kauf nehmen müssen. Wenn Tabellen aber unvorstellbare Ausmaße annehmen, sinkt unweigerlich die Abfrageleistung. Da dieser Fall eintritt, wenn viele Benutzer auf die Anwendung zugreifen, kann sich dies negativ auf Ihr Geschäft auswirken.

Sie können sich nun zwischen SQL und NoSQL entscheiden. Auf den ersten Blick scheint das eine das andere zu ersetzen. Bei SQL und NoSQL handelt es sich nicht um Varianten ein und derselben Technologie. Bei beiden geht es um die Persistenz von Daten, aber in Bezug auf die Struktur unterscheiden sie sich in vielerlei Hinsicht.

In NoSQL-Datenbanken werden keine festgelegten Datenschemas und Beziehungen verwendet. An sich geben sie kein Modell vor. Mit NoSQL-Datenbanken bleibt Ihnen das Modellieren des Domänenbereichs erspart. Die resultierenden Objekte können dauerhaft in logischen Gruppen gespeichert werden. Sie entwerfen ein Domänenmodell, arbeiten mit einem Objektdiagramm, und der NoSQL-Speicher muss nur noch die Objektserialisierung auf Datenträger durchführen.

Sie arbeiten mit Dokumenten?

In diesem Fall entspricht der Begriff „Dokument“ annähernd dem Begriff „Datensatz“. Dokumentsammlungen können Tabellen von Datensätzen wieder abrufen. Der wesentliche Unterschied besteht darin, dass jedes Objekt in einer Sammlung ein anderes Schema aufweist, als alle anderen Objekte in derselben Sammlung. Dennoch stehen alle Dokumente in logischer Beziehung zueinander.

Der Unterschied ist feiner, als es zunächst den Anschein hat. Angenommen, Sie verwalten die Biografien von Buchautoren. Möglicherweise steht Ihnen nicht für alle Autoren dieselbe Menge an Daten zur Verfügung. Einige der Daten zur Beschreibung von Autor1 weichen von denen zur Beschreibung von Autor2 ab. Wenn es sich bei der Biografie um eine Sammlung von Attributen handeln soll, haben Sie tatsächlich ein festgelegtes Schema mit einigen wenigen optionalen Spalten. Wenn es sich hingegen um eine Sammlung von Dateien wie einem Lebenslauf im Word-Format, einem XML-Datenstrom mit der Liste der veröffentlichten Bücher und Kommentare, Links zu Interviews auf YouTube und einigen wenigen Attributen wie Lebensdaten handeln soll, ist das Schema sehr viel weniger definiert und kaum geeignet für die starren Grenzen eines SQL-Speichers.

Das Ausmaß der Polymorphie Ihrer Daten ist ein ausgezeichneter Indikator, um zu entscheiden, inwiefern sich NoSQL in diesem Fall empfiehlt. Angenommen, Sie entwerfen ein System entsprechend einer Event-Sourcing-Architektur. In einer Event-Sourcing-Architektur ist das Persistenzmodell der Anwendung ganz einfach der Ereignisverlauf. Jede Benutzeraktion ruft serverseitig ein oder mehrere Ereignisse hervor. Um den Anwendungszustand nachzuverfolgen, müssen Sie lediglich diese Ereignisse aufzeichnen.

In einem E-Commerce-Szenario würde beispielsweise ein Workflow gestartet, wenn ein Benutzer eine Bestellung übermittelt. Dadurch wird möglicherweise eine Reihe von Domänenereignissen generiert: bestellung_übermittelt, bestellung_überprüft, bestellung_abgelehnt, bestellung_angelegt, bestellung_verarbeitet, bestellung_auf_dem_versandweg, bestellung_zugestellt, bestellung_zurückgesandt, bestellung_aktualisiert usw. All diese Ereignisse beziehen sich auf dieselbe Bestellung. Durch die Verarbeitung der Ereignissequenz können Sie den aktuellen Status der Bestellung erstellen bzw. erneut erstellen.

Bei der Verarbeitung von Bestellungen in einem E-Commerce-System geht es nicht nur darum, eine Bestelltabelle mit einer Statusspalte vorzuhalten. Es geht vielmehr darum, alle Aktionen nachzuverfolgen, die für die Daten ausgeführt wurden, die eine Bestellung repräsentieren. Dabei kann es sich um ganz unterschiedliche Aktionen handeln, die unterschiedliche Daten betreffen. Beispielsweise sind dem Ereignis „bestellung_zurückgesandt“ möglicherweise nur sehr wenige Daten zugeordnet. Das Ereignis „bestellung_übermittelt“ umfasst wahrscheinlich sämtliche mit dem betreffenden Kunden verbundenen Informationen. „bestellung_aktualisiert“ beinhaltet die Änderung, die an der vorhandenen Bestellung vorgenommen wurde.

Ihre Bestellung nimmt also eher die Form eines Dokuments an, da sie mithilfe einer Liste verschiedenartigster Ereignisse umfassend beschrieben wird. Die einzelnen Ereignisse sind einfach nur abzuspeichernde Objekte. Bei den meisten E-Commerce-Systemen kommt wahrscheinlich ein relationaler Speicher zum Einsatz, um diesen Anforderungen zu begegnen. Ein NoSQL-Ansatz für die Speicherung der Liste von Ereignissen könnte ziemlich interessant und vielversprechend sein. Weitere Informationen zur Event-Sourcing-Architektur finden Sie im E-Book „Exploring CQRS and Event Sourcing“, das Sie kostenlos im Microsoft Download Center unter bit.ly/1lesmzm herunterladen können.

Ist Eventual Consistency ein Problem?

Eventual Consistency ist ein weiterer wichtiger Unterschied zwischen SQL und NoSQL in Bezug auf die Struktur. Selbst wenn sich in Ihrem Datenmodell Dokumente einfach erkennen lassen, sind letztlich die Auswirkungen von Eventual Consistency auf die bereitgestellten Anwendungen ausschlaggebend für die endgültige Entscheidung.

Eventual Consistency bedeutet, dass Lese- und Schreibvorgänge nicht denselben Daten zugeordnet sind. Die meisten NoSQL-Systeme weisen Eventual Consistency auf (sind also irgendwann konsistent), da sie garantieren, dass eine Abfrage die Daten zurückgibt, die infolge des letzten Befehls geschrieben wurden, sofern über einen hinreichenden Zeitraum keine Aktualisierungen an einem bestimmten Objekt vorgenommen wurden.

In den meisten Fällen stellt Eventual Consistency kein Problem dar. Generell ist innerhalb eines gebundenen Kontexts höchstmögliche Konsistenz erforderlich, kontextübergreifend jedoch ist eigentlich keine Konsistenz geboten. Mit zunehmender Größe Ihres Systems wird es – trotz der Technologie – schwieriger, sich auf die Konsistenz zu verlassen.

Ob Eventual Consistency ein Problem darstellt, lässt sich mithilfe eines einfachen Tests herausfinden. Wie würden Sie ein Szenario beurteilen, in dem ein Befehl bestimmte Daten schreibt, bei einem anschließenden Lesevorgang jedoch veraltete Daten zurückgegeben werden? Wenn es für Sie entscheidend ist, dass Sie stets lesen können, was gerade geschrieben wurde, haben Sie zwei Möglichkeiten:

  • Sie vermeiden die Verwendung von NoSQL-Datenbanken.
  • Sie konfigurieren die NoSQL-Datenbank so, dass sie konsistent ist.

Werfen Sie einen Blick auf den folgenden Codeausschnitt, dem die Verwendung einer RavenDB – einer verbreiteten .NET-basierten NoSQL-Datenbank – zugrunde liegt:

DocumentSession
  .Store(yourObject);
DocumentSession
  .Query<YourObjectType>()
  .Where(t => t.Id == id)

Mit der ersten Zeile wird ein Objekt im RavenDB-Archiv gespeichert. Anschließend soll das Objekt wiederum gelesen werden, indem für dieselbe Speichersitzung der Wert einer ID-Eigenschaft des gespeicherten Objekts abgefragt wird. Bei einer standardmäßigen Datenbankkonfiguration wird nicht dasselbe gelesen, was gerade zuvor geschrieben wurde.

Bei einer RavenDB handelt es sich beim Schreibvorgang im Speicher und der Aktualisierung der vom Abfragemodul verwendeten Indizes um zwei unterschiedliche Vorgänge. Die Indexaktualisierungen sind geplante Vorgänge. Diese Diskrepanz besteht nur wenige Sekunden, sofern in der Zwischenzeit keine weiteren Aktualisierungen am selben Objekt vorgenommen werden. So können Sie eine strikte Konsistenz erzwingen:

_instance.Conventions.DefaultQueryingConsistency =
  ConsistencyOptions.AlwaysWaitForNonStaleResultsAsOfLastWrite;

Allerdings gibt der Lesevorgang in diesem Fall erst dann Daten zurück, wenn der Index aktualisiert wurde. Es kann also einige Sekunden dauern, bis ein einfacher Lesevorgang abgeschlossen ist. Hier zeigt sich, dass NoSQL-Datenspeicher einen Nischenplatz in der Branche besetzen. Sie bergen jedoch einige Herausforderungen. NoSQL lösen einige architekturbezogene Probleme, lassen andere aber außer Acht.

Es gibt bessere Möglichkeiten, diesen Wartezeiten zu begegnen. Welche das sind, hängt von Ihrem jeweiligen Szenario ab. Aus diesem Grund empfiehlt es sich, zum Zeitpunkt der Abfrage zu entscheiden, welche Art von Konsistenz für eine Abfrage erforderlich ist, wie im folgenden Beispiel dargestellt:

using( var session = store.OpenSession() )
{
  var query = session.Query<Person>()
    .Customize(c=> c.WaitForNonStaleResultsAsOfLastWrite() )
    .Where( p => /* condition */ );
}

Hier weist die WaitForNonStaleResultsAsOfLastWrite-Abfrageanpassung den Server an, darauf zu warten, dass der relevante Index das zuletzt geschriebene Dokument indiziert hat, und Dokumente zu ignorieren, die auf der Serverseite ankommen, nachdem die Abfrage ausgegeben wurde.

Dies bietet sich für Szenarien mit sehr vielen Schreibvorgängen an, in denen die Indizes immer veraltet sind. Das liegt in der Natur der Sache. Es gibt zahlreiche WaitForNonStaleResultsXxxx-Methoden mit unterschiedlichem Verhalten, die für jeweils unterschiedliche Szenarien geeignet sind. Ein weitere Möglichkeit wäre, sich ganz auf die Eventual Consistency einzulassen und einfach vom Server abzufragen, ob es sich um veraltete Ergebnisse handelt. Gehen Sie dazu folgendermaßen vor:

using( var session = store.OpenSession() )
{
  RavenQueryStatistics stats;
  var query = session.Query<Person>()
    .Statistics( out stats )
    .Where( p => /* condition */ );
}

In diesem Beispiel wird nicht auf Indizes gewartet. Sie rufen vom Server zusätzlich die Abfragestatistiken ab, denen Sie entnehmen können, ob die zurückgegebenen Ergebnisse veraltet sind. Für das gegebene Szenario sollten Ihnen damit genügend Informationen zur Verfügung stehen, um die bestmögliche Entscheidung zu treffen.

Polyglot Persistence

Letzten Endes geht es nicht darum, das Ende des relationalen Speichers auszurufen und diesen durch ein NoSQL-Produkt der Wahl zu ersetzen. E geht vielmehr darum, die Mechanismen des Systems und die Eigenschaften der Daten zu erfassen und anhand dieser Kenntnisse die bestmögliche Architektur zu ermitteln. Die am ehesten absehbare konkrete Anwendung für NoSQL-Speicher ist als Ereignisspeicher im Kontext einer Event-Sourcing-Architektur.

Für Systeme, bei denen es keine eindeutige Antwort auf die Frage gibt, ob sich relationale Speicher empfehlen, sollten Sie „Polyglot Persistence“ erwägen. Anstatt sich entweder für NoSQL-Datenbanken oder für relationale Datenbanken zu entscheiden, können Sie eine Speicherschicht in Betracht ziehen, bei der die Vorzüge beider Technologien kombiniert werden. Mit diesem Speichersystem können Sie unterschiedlichen Problemen auf die jeweils am besten geeignete Weise begegnen.

Im Kontext eines Unternehmens sollten Sie unterschiedliche Speichertechnologien für unterschiedliche Datentypen in Erwägung ziehen. Dies gilt insbesondere für eine dienstorientierte Architektur. In diesem Fall hätte jeder Dienste eine eigene Speicherschicht. Es gäbe also keinen Grund, den Speicher zu vereinheitlichen und sich für ein einziges Produkt bwz. eine einzige Technologie zu entscheiden. Polyglot Persistence erfordert jedoch, dass Sie sich mit unterschiedlichen Speichertechnologien und -produkten vertraut machen. Die Investition in die Einarbeitung macht sich jedoch bezahlt.


Dino Esposito ist Mitautor von „Microsoft .NET: Architecting Applications for the Enterprise“ (Microsoft Press, 2014) und „Programming ASP.NET MVC 5“ (Microsoft Press, 2014). Esposito ist Technical Evangelist für die .NET Framework- und Android-Plattformen bei JetBrains und spricht häufig auf Branchenveranstaltungen weltweit. Auf software2cents.wordpress.com und auf Twitter unter twitter.com/despos lässt er uns wissen, welche Softwarevision er verfolgt.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Mauro Servienti (Managed Designs)