Zum Inhalt springen
Deutsch

Daten gestalten

CRDTs sind mächtig — aber nur, wenn die Daten passen. Der falsche Typ produziert korrekte Merges von für-deine-App inkorrekten Werten. Diese Seite ist der Entscheidungs-Leitfaden für die Wahl des richtigen CRDT oder die Erkenntnis, dass CRDTs gar nicht passen.

Was sind die Daten?CRDT
Eine einzelne Zahl, die nur wächstGCounter
Eine einzelne Zahl, die hoch- und runtergehtPNCounter
Ein Einzelwert, Last-Writer-WinsLWWRegister<T>
Ein Einzelwert, nebenläufige Writes erkennenMVRegister<T>
Ein Set, das nur wächstGSet<E>
Ein Set mit Adds und RemovesORSet<E>
Eine Map Key → Einzelwert LWWLWWMap<K, V>
Eine Map Key → CounterGCounterMap<K>
Eine Map Key → CRDTORMap<K, C>
Sonst (reichhaltige/strukturierte Daten, transaktionale Updates, geordnete Listen)Kein CRDT-Fit — siehe unten

Lege die Entscheidung früh fest. Den CRDT-Typ später zu wechseln erfordert eine Migration — das Wire-Format unterscheidet sich pro Typ, und gespeicherte Daten sind nicht über Typen hinweg kompatibel.

// Set aktuell online befindlicher User-IDs:
ORSet<string>

User verbinden (add) und disconnecten (remove). Nebenläufiges Connect-aus-verschiedenen-Clients ist “Add wins” — genau das willst du, wenn ein User einen zweiten Tab öffnet, während der erste noch aktiv ist.

GCounterMap<string>; // string = Page-URL oder -ID

Clicks gehen nur nach oben; du willst Counts pro Page. Bei jedem Click inkrementieren; per Page-URL lesen.

LWWMap<UserId, UserPrefs>; // UserPrefs ist deine Pref-Form

Ein-Wert-Blob pro User. Der letzte Write gewinnt — okay für “User hat sein Theme in zwei Tabs geändert, der letzte Save bleibt erhalten”.

Für feingranulares Pref-Editing, bei dem nebenläufige Änderungen verschiedener Felder beide bleiben sollen: nimm ORMap<UserId, LWWMap<FieldName, FieldValue>> — jedes Feld ist unabhängig LWW.

ORMap<UserId, ORSet<ItemId>>;

Mutables Set von Items pro User. Nebenläufiges Add-und-Remove desselben Items: Add gewinnt (User hat auf Tab A hinzugefügt, während auf Tab B geleert wurde → Item bleibt drin).

PNCounter;

Sessions kommen und gehen; die Netto-Anzahl ist interessant. Nimm PNCounter (nicht GCounter!), weil Sessions auch disconnecten.

View-Counter pro Ressource, der täglich zurückgesetzt wird

Abschnitt betitelt „View-Counter pro Ressource, der täglich zurückgesetzt wird“
// Kein toller CRDT-Fit — "Reset" ist keine natürliche CRDT-Operation.

Stattdessen: GCounter wie er ist speichern; aktuellen Tag in separatem LWWRegister; beim Lesen den Wert zu “Tagesbeginn” subtrahieren. Das CRDT wächst weiter; die user-sichtbare Zahl scheint zurückzusetzen.

Manche Formen sehen CRDT-freundlich aus, passen aber nicht:

  • Hohe Key-Kardinalität — ein DD-Eintrag pro Session für 10 M Sessions = 10 M Einträge × N Nodes. Nimm stattdessen Sharding.
  • Große Werte — ein 50 MB-Dokument bei jeder Gossip-Runde an jeden Node zu replizieren ist Verschwendung. Speichere das Dokument extern, repliziere einen Pointer.
  • Häufige Writes aus einer einzigen Quelle — DDs Gossip amortisiert über viele Writer; bei einem heißen Writer zahlst du die Gossip-Kosten ohne Gegenwert. Nimm einen lokalen PersistentActor.
  • Strikt transaktionale Semantik — DD kennt keine Isolation-Level.

Echte Apps kombinieren oft CRDTs:

// Konfiguration pro Tenant:
// - tenantId → { features, quota }
// - features ist ein Set
// - quota ist LWW
type Features = ORSet<string>;
type Quota = LWWRegister<number>;
type Tenant = ORMap<string, Features | Quota>;
const tenants = ORMap.empty<string, Tenant>();

Das Muster: das CRDT für jedes Leaf nach seiner Semantik wählen; unter ORMap schachteln (der flexibelste Container).

Für homogene Nests (jeder Tenant hat identische Struktur) lesen sich mehrere Top-Level-Maps oft sauberer:

const features = ORMap.empty<string, ORSet<string>>(); // tenantId → features
const quotas = LWWMap.empty<string, number>(); // tenantId → quota

Trade-off: Komposition gibt atomare Operationen pro Tenant; geteilte Maps geben einfachere Typen, aber Cross-Map-Inkonsistenz ist möglich (Quota aktualisiert, Features noch nicht propagiert).