NoSQL-Familien, Replikation und Quorum
Track Konzepte · Datenbanken verteilen · ca. 50 Min.
Worum es geht
Eine einzelne Datenbank-Maschine hat Grenzen: Sie kann ausfallen, sie schafft nur begrenzt viele Anfragen, und ihre Daten passen irgendwann nicht mehr auf eine Platte. Eine Antwort darauf heißt Replikation (replication: dieselben Daten auf mehreren Maschinen). Sie kostet etwas: Kopien sind nie ganz gleichzeitig aktuell. Dazu kommt die Frage, welche NoSQL-Familie (Key-Value, Document, Wide-Column, Graph, Time-Series) zu welchem Zugriffsmuster passt.
Am Ende von Teil 1 kannst du eine NoSQL-Familie aus Randbedingungen wählen, erklären, warum du direkt nach dem Speichern deine eigene Änderung manchmal nicht siehst (und wie du das reparierst), und wann ein Quorum (R + W > N) Überlappung garantiert. Das Aufteilen der Daten (Sharding) und das CAP-Theorem folgen in Teil 2.
Du siehst hier keine echten Server. Du baust einen Fake-Cluster mit einer Uhr als Zähler (jeder tick() ist ein Zeitschritt). So ist jeder Fehler jedes Mal gleich reproduzierbar, wie der Simulator in Lektion 01.
Von JS/TS her gedacht
Du kennst das Problem aus dem Frontend: Du speicherst ein Profil (POST), lädst die Seite neu (GET), und kurz siehst du noch den alten Namen. Mit React Query löst du das oft mit einem optimistischen Update oder mit invalidateQueries. Auf der Server-Seite steckt dahinter meist Replication Lag (Replikationsverzögerung): Das GET landet auf einer Kopie, die die Änderung noch nicht hat.
| Du kennst | Verteilte Datenbank |
|---|---|
Map im Speicher |
Key-Value-Store |
| JSON-Objekt pro Eintrag | Document Store |
| Cache-Kopie neben dem Original | Follower (Replikat) neben dem Leader |
| Optimistisches Update im Client | Read-your-writes per Versions-Token |
Antworten mehrerer Server einsammeln (Promise.all) und die neueste Version nehmen |
Quorum-Lesen: R Replikate fragen, höchste Version gewinnt |
Konzept
Schritt 1: Die NoSQL-Familien und ihr Einsatzfall
“NoSQL” ist kein einheitliches Ding, sondern ein Sammelbegriff für Datenmodelle, die jeweils ein bestimmtes Zugriffsmuster (access pattern) gut können und dafür anderes opfern. Du wählst nicht nach Modeerscheinung, sondern nach der Frage: Wie lese ich später?
| Familie | Datenmodell | Passender Einsatzfall | Preis |
|---|---|---|---|
| Key-Value | Schlüssel, Wert (undurchsichtig) | Sessions, Caches, Rate-Limits, Feature Flags | Abfrage nur über den Schlüssel |
| Document | JSON-ähnliche Dokumente mit flexiblem Schema | Produktkataloge, Benutzerprofile, Inhalte, die als Ganzes gelesen werden | Joins über Dokumente sind schwach |
| Wide-Column | Zeilen mit Partition Key und sortierten Spalten | Sehr hohe Schreiblast, Zugriff immer nach Partition Key plus Bereich (z. B. Aktivitätsprotokolle) | Abfragen müssen im Voraus zum Datenmodell passen |
| Graph | Knoten und Kanten | Beziehungen über mehrere Stufen (Netzwerke, Empfehlungen, Betrugserkennung) | Weniger geeignet für reine Massendaten-Aggregation |
| Time-Series | Messwerte mit Zeitstempel | Metriken, Sensoren, Monitoring: anhängen, nach Zeitfenstern aggregieren, alte Daten ablaufen lassen | Nachträgliches Ändern einzelner Punkte ist unüblich |
Das ist ein Entscheidungsproblem mit Randbedingungen, genau das übst du in Übung 1. Produktnamen (Redis, MongoDB, Cassandra, Neo4j, InfluxDB und andere) sind nur Beispiele für die Familien, die Auswahl eines Produkts ist ein eigenes Thema.
Schritt 2: Replikation und der Fake-Cluster
Bei Replikation gibt es in der häufigsten Form (Single-Leader, auch Primary/Replica) einen Leader, der alle Schreibvorgänge annimmt, und mehrere Follower, die die Änderungen vom Leader nachgeliefert bekommen. Gelesen werden darf von den Followern, das entlastet den Leader. Das Nachliefern dauert aber, und diese Verzögerung heißt Replication Lag.
Hier der Fake-Cluster. Jeder Follower hat eine eigene Verzögerung in Ticks. schreibe gibt die Versionsnummer des Schreibvorgangs zurück, die brauchst du gleich.
Ein Schreibvorgang bei Tick 0, danach liest du bei jedem Tick vom Leader und von beiden Followern (Lag 1 und Lag 4):
Du siehst: Der Leader kennt den Wert sofort. Follower 0 hat ihn ab Tick 1, Follower 1 erst ab Tick 4. Dazwischen liefert ein Follower (None, 0), also “kenne ich nicht”. Das ist Eventual Consistency (letztendliche Konsistenz): Wenn niemand mehr schreibt, werden irgendwann alle Kopien gleich. Wann genau, sagt dir niemand.
Schritt 3: Der Read-your-writes-Bug
Ein einfacher Client schreibt über den Leader und liest über irgendeinen Follower (so verteilt ein Load Balancer die Last):
Direkt nach dem Speichern liefern beide Follower None. Nach Tick 1 hängt es davon ab, welchen Follower du gerade erwischst: 'Ayse' und None. Erst nach Tick 4 stimmt es immer. Der Nutzer erlebt: “Ich habe gespeichert und es ist weg”, beim zweiten Neuladen ist es da. Das verletzt Read-your-writes (Lesen der eigenen Schreibvorgänge). Dass ein zweites Lesen älter aussieht als das erste, heißt Monotonic Reads verletzt.
Zwei übliche Reparaturen:
- Nach eigenem Schreiben vom Leader lesen (für eine Weile oder für genau diesen Key). Einfach, aber der Leader bekommt mehr Last.
- Versions-Token: Der Client merkt sich die Versionsnummer seines letzten Schreibvorgangs je Key. Beim Lesen akzeptiert er nur eine Antwort mit
version >= token. Ist der Follower noch zu alt, fragt er den Leader. Alle anderen Lesevorgänge bleiben bei den Followern.
Dasselbe Prinzip in TypeScript (hier als JavaScript mit Node ausgeführt, nur die Typannotationen weggelassen):
const leader = new Map<string, { wert: string; version: number }>();
const follower = new Map<string, { wert: string; version: number }>();
let version = 0;
function schreibe(key: string, wert: string): number {
version++;
leader.set(key, { wert, version });
setTimeout(() => follower.set(key, { wert, version: leader.get(key)!.version }), 50);
return version;
}
function lies(key: string, minVersion = 0): string | undefined {
const f = follower.get(key);
if (f && f.version >= minVersion) return f.wert; // Follower reicht
return leader.get(key)?.wert; // sonst Leader
}
const v = schreibe("name", "Ayse");
console.log("ohne Token:", follower.get("name")?.wert); // undefined
console.log("mit Token: ", lies("name", v)); // AyseDie Ausgabe war ohne Token: undefined und mit Token: Ayse. In einer HTTP-API wandert die Versionsnummer typischerweise als Header oder Cookie zum Client und mit der nächsten Leseanfrage zurück. Das Konzept ist sprachunabhängig.
Schritt 4: Replikationsarten und Quorum
| Art | Wer nimmt Schreibvorgänge an? | Stärke | Preis |
|---|---|---|---|
| Single-Leader | genau ein Leader | einfach, klare Reihenfolge | Leader ist Engpass, Failover nötig |
| Multi-Leader | mehrere Leader (z. B. je Region) | Schreiben in der Nähe, Ausfall einer Region verkraftbar | Schreibkonflikte müssen aufgelöst werden |
| Leaderless | jeder Knoten, Client schreibt an mehrere | kein einzelner Ausfallpunkt | Client oder Koordinator muss Quorum rechnen, Konflikte möglich |
Bei Leaderless (Dynamo-Stil) gibt es N Replikate. Ein Schreibvorgang gilt als erfolgreich, wenn W Replikate ihn bestätigt haben. Ein Lesevorgang fragt R Replikate und nimmt die Antwort mit der neuesten Version. Die Regel:
Gilt R + W > N, überlappen sich jede Schreib-Gruppe und jede Lese-Gruppe in mindestens einem Replikat. Dieses eine Replikat kennt die neueste Version, also siehst du sie beim Lesen.
Beispiel mit N = 3 (Replikate 0, 1, 2), alle Möglichkeiten ausgeschrieben:
- W = 2: Das Schreiben trifft
{0,1},{0,2}oder{1,2}. R = 2: Das Lesen trifft ebenfalls eine dieser drei Gruppen. Je zwei dieser Gruppen haben mindestens ein Replikat gemeinsam, z. B.{0,1}und{1,2}teilen Replikat 1. Das ist die Überlappung (2 + 2 = 4 > 3). - W = 1, R = 1: Das Schreiben trifft z. B. nur
{0}, das Lesen nur{2}. Kein gemeinsames Replikat, du siehst die neue Version nicht (2 ist nicht größer als 3).
In Übung 3 probierst du solche Kombinationen für beliebige N, W, R automatisch durch und baust dazu die Lesefunktion.
Ein Quorum garantiert nur die Überlappung. Es macht aus dem System keine Datenbank mit Transaktionen: Gleichzeitige Schreibvorgänge und halb fehlgeschlagene Schreibvorgänge (weniger als W Bestätigungen) bleiben eigene Probleme.
Falle
- “Wir nehmen NoSQL, weil es skaliert.” Ohne Zugriffsmuster ist das keine Begründung. Ein Document Store ohne Joins ist schlecht, wenn deine Fachlichkeit aus Beziehungen besteht.
- Von Followern lesen und trotzdem erwarten, dass du die eigene Änderung siehst. Das ist der Read-your-writes-Bug. Er fällt in Tests mit kleinem Lag nie auf und in Produktion unter Last.
- R + W > N heißt nicht “konsistent wie eine Transaktion”. Es heißt nur: Die Lese-Gruppe enthält mindestens ein Replikat mit der neuesten bestätigten Version.
Übungen
Übung 1: Trade-off-Fallstudie, NoSQL-Familie wählen (ca. 8 Min.)
Ordne jedem der vier Fälle die passende Familie zu. Die Randbedingungen stehen im Text und reichen für eine eindeutige Wahl.
Optionen für jeden Fall:
- A Key-Value
- B Document
- C Wide-Column
- D Graph
- E Time-Series
Fall 1: Produktkatalog. Ein Shop verkauft Schuhe (Größen, Farben, Material) und Laptops (CPU, RAM, Anschlüsse). Jede Produktart hat völlig andere Felder, und neue Arten kommen laufend dazu. Jede Produktseite lädt ein Produkt als Ganzes über seine ID. Der Einkauf will außerdem nach einzelnen Feldern innerhalb einer Produktart filtern können (z. B. alle Schuhe in Größe 42). Abfragen, die Joins zwischen Produkten brauchen, gibt es nicht.
Fall 2: Login-Sessions. Der Zugriff erfolgt ausschließlich über die Session-ID. Die Antwort muss in unter einer Millisekunde kommen. Jede Session verfällt automatisch nach 30 Minuten ohne Aktivität. Geht eine Session verloren, loggt sich der Nutzer eben neu ein.
Fall 3: Sensordaten. 20 000 Temperaturmesswerte pro Sekunde, nur anhängen, nie ändern. Abgefragt wird fast immer “Durchschnitt pro Stunde über die letzten 7 Tage je Sensor”. Daten, die älter als 90 Tage sind, sollen automatisch verschwinden.
Fall 4: Kontaktempfehlungen. Für einen Nutzer sollen “Bekannte von Bekannten von Bekannten” (bis zu vier Stufen Beziehung) gefunden werden. Die Beziehungen sind wichtiger als die einzelnen Datensätze, und die Abfrage folgt immer wieder Kanten von Person zu Person.
Frage dich pro Fall: Wie wird gelesen (nur nach Schlüssel, ganzes Dokument, Zeitfenster, Kanten folgen)? Und welche Eigenschaft muss das System von selbst mitbringen (Ablauf, flexibles Schema, Aggregation)?
antwort = ("B", "A", "E", "D")
antwortFall 1: Document (flexibles Schema, ein Dokument pro Produkt, Zugriff als Ganzes). Fall 2: Key-Value (nur Zugriff über Schlüssel, Ablaufzeit, sehr schnell). Fall 3: Time-Series (anhängen, Zeitfenster-Aggregation, automatischer Ablauf alter Daten). Fall 4: Graph (Kanten über mehrere Stufen folgen).
Übung 2: Read-your-writes reparieren (ca. 12 Min.)
Der naive Client aus Schritt 3 verletzt Read-your-writes. Baue Sitzung so um, dass ein Client seine eigenen Schreibvorgänge immer sieht, aber trotzdem nicht jede Lesung auf den Leader legt:
schreibe(key, wert)schreibt überself.cluster.schreibe(...)und merkt sich die zurückgegebene Version je Key inself.token.lies(key)liest zuerst von einem Follower (self.cluster.lies_follower(key), liefert(wert, version)). Ist die Version kleiner als das Token für diesen Key, liest es vom Leader (self.cluster.lies_leader(key)). Rückgabe ist nur der Wert.- Für Keys, die dieser Client nie geschrieben hat, darf nie der Leader gefragt werden (kein Token, also genügt jeder Follower).
Ein Token gilt pro Key und muss bei jedem eigenen Schreibvorgang auf die neue Version gesetzt werden, nicht nur beim ersten. Vergleiche Versionen, nicht Werte: Auch ein alter Wert ist ein Wert.
class Sitzung:
def __init__(self, cluster):
self.cluster = cluster
self.token = {} # key -> Version meines letzten Schreibvorgangs
def schreibe(self, key, wert):
self.token[key] = self.cluster.schreibe(key, wert)
def lies(self, key):
wert, version = self.cluster.lies_follower(key)
if version < self.token.get(key, 0):
wert, version = self.cluster.lies_leader(key)
return wert
SitzungDer Follower wird nur übergangen, wenn er hinter dem eigenen Schreibvorgang zurückliegt. Sobald er aufgeholt hat, bleibt der Leader unbelastet.
Übung 3: Quorum selbst nachrechnen (ca. 12 Min.)
Du schreibst zwei kleine Funktionen:
garantiert(n, w, r): GibtTruezurück, wenn bei N Replikaten, Schreiben auf W und Lesen von R Replikaten jede Lese-Gruppe die neueste Version sieht, egal welche Replikate beim Schreiben und Lesen drankommen.lese(replikate, indizes):replikate[i]ist ein Tupel(wert, version). Frage nur die Replikate inindizesab und gib den Wert mit der höchsten Version zurück.
Der Check probiert alle Kombinationen aus (alle möglichen Schreib-Gruppen und Lese-Gruppen) und vergleicht mit deiner Formel.
Schreib die Gruppen für ein kleines N auf (siehe Beispiel in Schritt 4) und suche nach dem Muster. Und beim Lesen: Woran erkennst du, welches Replikat das neueste ist? Der Wert selbst sagt es nicht.
def garantiert(n, w, r):
return r + w > n
def lese(replikate, indizes):
wert, version = max((replikate[i] for i in indizes), key=lambda p: p[1])
return wert
garantiert, lesemax mit key nach Version wählt das neueste Replikat. Der Wert selbst (hier Texte wie "v1") sagt nichts darüber, welcher neuer ist.
Merksatz
Du wählst die NoSQL-Familie nach dem Zugriffsmuster, und Replikation kauft Verfügbarkeit mit Konsistenz: Wer von Followern liest, sieht die eigene Änderung nicht automatisch, und ein Quorum mit R + W > N garantiert nur die Überlappung, keine Transaktion.
Prüfstein
Dein Nutzer speichert sein Profil und sieht danach kurz den alten Namen. Erkläre die Ursache, nenne zwei Reparaturen mit ihren Kosten, und sage, warum ein Quorum mit R + W > N das Problem im leaderless Fall löst, aber keine Transaktion ersetzt.
Weiter mit Teil 2: Sharding und CAP.
Quelle: quellen/konzeptuebersicht-software-grundlagen.md, Abschnitt “5. Datenbanken” (NoSQL-Familien mit Einsatzfall; Replikation, Eventual Consistency); quellen/konzeptuebersicht-software-fortgeschritten.docx, Schicht 5 “Replikation” (Single-Leader, Multi-Leader, Leaderless, Replication Lag, Quorum, Read-your-writes). Beide Quellen sind Stichwortlisten. Über die Quelle hinaus (allgemeines Fachwissen): Beschreibung der Einsatzfälle und Preise der NoSQL-Familien, Produktbeispiele, die Reparaturen für Read-your-writes (Leader-Lesen, Versions-Token), Monotonic Reads, die Quorum-Regel R + W > N samt Einschränkungen. Alle Zahlen und Ausgaben im Text stammen aus ausgeführtem Code (Python 3, TypeScript-Beispiel als JavaScript mit Node 20). Konkrete Standardwerte oder Verhalten einzelner Datenbankprodukte werden nicht genannt (bitte prüfen, falls ergänzt).