Distributed Data im Überblick
Distributed Data ist die Schicht für eventually consistent shared state im Cluster. Jeder Node hält eine lokale Replika jedes Keys. Updates werden zuerst lokal angewendet; Gossip propagiert sie; konfliktierende nebenläufige Updates mergen automatisch über die CRDT-Semantik des Datentyps.
Der geteilte Zustand ist ein CRDT — Conflict-free Replicated Data Type. Eine Handvoll sorgfältig entworfener Typen (Counter, Sets, Register, Maps), deren Merge-Operation kommutativ, assoziativ und idempotent ist: Gossip darf Updates in beliebiger Reihenfolge zustellen, sie wiederholen oder verlieren — die Replikas konvergieren trotzdem zum selben Wert.
Wann du DistributedData einsetzt
Abschnitt betitelt „Wann du DistributedData einsetzt“Drei Muster:
- Cluster-weite Counter und Gauges — Gesamtzahl der Requests, aktive Sessions, aktuelles Rate Limit. Jeder Node kann lesen und schreiben, ohne sich abzustimmen; der gemergte Wert ist die globale Wahrheit.
- Mengen für Membership — “welche Sessions sind aktiv”, “welche Feature Flags sind aktiviert”. Elemente von jedem Node hinzufügen und entfernen; die Set-Semantik kümmert sich korrekt um nebenläufige Adds.
- Konfigurations-Register — ein einzelner Wert, den Nodes schreiben und lesen; Timestamps entscheiden bei nebenläufigen Writes über den Gewinner.
Ein minimales Beispiel
Abschnitt betitelt „Ein minimales Beispiel“import { ActorSystem, Cluster } from 'actor-ts';import { DistributedDataId, GCounter } from 'actor-ts';
const system = ActorSystem.create('my-app');const cluster = await Cluster.join(system, { host, port, seeds });const dd = system.extension(DistributedDataId).start(cluster);
// Counter inkrementieren — keine Koordination, einfach in die lokale Replika mergen.dd.update<GCounter>( 'request-count', GCounter.empty, (c) => c.increment(dd.selfReplicaId(), 1),);
// Lokale Sicht lesen — liefert den letzten bekannten gemergten Stand.const counter = dd.get<GCounter>('request-count');console.log(counter?.value); // Summe über alle bekannten ReplikasDas lokale Update ist sofort wirksam; Gossip propagiert es in den nächsten Runden zu den Peers. Reads sind immer lokal (billig, kein Netzwerk) und liefern, was die lokale Replika aktuell weiß — was zum globalen Stand konvergiert.
Die CRDT-Typen
Abschnitt betitelt „Die CRDT-Typen“| Typ | Was es ist | Wann |
|---|---|---|
GCounter | Grow-only Counter. Jede Replika zählt ihren eigenen Beitrag; der Wert ist die Summe. | Counts, die nur steigen — Impressions, abgeschlossene Jobs. |
PNCounter | Counter mit Inkrement und Dekrement (intern zwei GCounter). | Counts, die in beide Richtungen gehen — aktive Sessions. |
GSet | Grow-only Set. Nur Adds, keine Removes. | Append-only Collections — beobachtete Event-Typen, gesehene User. |
ORSet | Observed-Remove Set. Adds und Removes; nebenläufiges Add+Remove löst sich zu Add auf (das Add wurde beobachtet). | Membership-Sets, in denen Elemente kommen und gehen. |
LWWRegister<T> | Last-Writer-Wins Register. Ein einzelner Wert, Gewinner ist der jüngste Timestamp. | Konfig mit einem Wert — Feature Flag, letzter bekannter Leader. |
MVRegister<T> | Multi-Value Register. Nebenläufige Writes werden als Set gehalten; der Caller wählt. | Wenn du erkennen musst, dass “zwei Replikas gleichzeitig geschrieben haben”. |
LWWMap<K, V> | Eine Map von K auf LWW-Werte. | Konfig pro Key mit einem Wert. |
ORMap<K, C> | Eine Map, deren Werte selbst CRDTs sind. | Counter pro Key, Sets pro Key. |
GCounterMap<K> | Convenience: eine Map von GCounters. | Klicks pro Key, Requests pro Tenant. |
In CRDT-Typen findest du den
Deep Dive zu jedem — merge()-Semantik, wann sie passen, wann
nicht.
Konsistenz-Stellschrauben
Abschnitt betitelt „Konsistenz-Stellschrauben“Reads und Writes haben beide einen Consistency-Parameter:
await dd.updateAsync('hits', GCounter.empty, (c) => c.increment(dd.selfReplicaId(), 1), { consistency: 'majority', timeoutMs: 2_000 });
const hits = await dd.getAsync<GCounter>('hits', { consistency: 'majority' });| Level | Was es bedeutet |
|---|---|
'local' (Default) | Lokal anwenden; auf Gossip vertrauen, dass es propagiert. Read liefert die Sicht der lokalen Replika. |
'majority' | Warten, bis ⌈N/2 + 1⌉ Replikas geACKt haben. Read merged die Mehrheitsantworten. |
'all' | Auf jedes Up-Member warten. Stärkste Konsistenz, höchste Latenz. |
{ kind: 'count'; n: 3 } | Auf genau n ACKs warten. |
Quorum ändert die Merge-Semantik nicht — jedes Update merged irgendwann in jede Replika. Quorum gibt dir nur eine Garantie, wann genügend Replikas den Write gesehen haben.
Für die meisten Reads und Writes ist 'local' der richtige
Default. Greife zu 'majority', wenn:
- Ein Read einen kürzlich von dir geschriebenen Write
reflektieren soll —
'local'funktioniert nur, wenn du auf demselben Node liest, auf dem du geschrieben hast; Majority Reads decken den knotenübergreifenden Fall ab. - Ein Write “durable” sein soll, bevor du dem Client antwortest
—
'majority'stellt sicher, dass ein Node-Ausfall ihn nicht verliert.
Auf Änderungen subscriben
Abschnitt betitelt „Auf Änderungen subscriben“const unsubscribe = dd.subscribe<GCounter>('hits', (counter) => { console.log(`hits liegt jetzt bei ${counter.value}`);});
// ... späterunsubscribe();Der Callback feuert synchron nach jedem erfolgreichen Update oder Merge, der den lokalen Wert ändert. Nutze ihn, um DistributedData mit dem Rest deiner App zu verdrahten — etwa um ein UI bei Änderungen zu aktualisieren oder Folge-Logik anzustoßen, wenn ein Counter eine Schwelle überschreitet.
Persistenz
Abschnitt betitelt „Persistenz“Standardmäßig ist DistributedData nur in-memory. Wenn der ganze Cluster neu startet (Cold Start), beginnt jeder Key wieder leer.
Für Restart-überlebende Semantik nutzt du die durable-Variante:
import { DurableDistributedDataStore } from 'actor-ts';
const dd = system.extension(DistributedDataId).start(cluster, { durable: new DurableDistributedDataStore({ durableStateStore: someStore, keys: ['hits', 'config'], // nur diese Keys werden persistiert }),});Die Durable-Schicht persistiert Änderungen auf Disk (oder wohin auch immer der Durable-State-Store schreibt); beim Neustart wird der Stand der Replika wiederhergestellt, bevor sie Gossip beitritt.
Siehe Durable Storage für die Konfigurationsdetails.
Wann du DistributedData NICHT einsetzt
Abschnitt betitelt „Wann du DistributedData NICHT einsetzt“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- CRDT-Typen — Deep Dive zur Semantik jedes Typs.
- Replikation — wie Gossip Updates zwischen Replikas verteilt.
- Quorum Reads/Writes — die Konsistenz-Stellschrauben im Detail.
- Durable Storage — für State, der Restarts überlebt.
- Cluster im Überblick — das Membership-Modell darunter.
- Sharding im Überblick — die Per-Key-Alternative, wenn Keys zu zahlreich für Replikation sind.
Die DistributedData-API-Referenz
deckt die vollständige Extension-Oberfläche ab.