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.
Der Entscheidungsbaum
Abschnitt betitelt „Der Entscheidungsbaum“| Was sind die Daten? | CRDT |
|---|---|
| Eine einzelne Zahl, die nur wächst | GCounter |
| Eine einzelne Zahl, die hoch- und runtergeht | PNCounter |
| Ein Einzelwert, Last-Writer-Wins | LWWRegister<T> |
| Ein Einzelwert, nebenläufige Writes erkennen | MVRegister<T> |
| Ein Set, das nur wächst | GSet<E> |
| Ein Set mit Adds und Removes | ORSet<E> |
| Eine Map Key → Einzelwert LWW | LWWMap<K, V> |
| Eine Map Key → Counter | GCounterMap<K> |
| Eine Map Key → CRDT | ORMap<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.
Häufige Formen
Abschnitt betitelt „Häufige Formen“Online-Präsenz
Abschnitt betitelt „Online-Präsenz“// 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.
Click-Counts pro Page
Abschnitt betitelt „Click-Counts pro Page“GCounterMap<string>; // string = Page-URL oder -IDClicks gehen nur nach oben; du willst Counts pro Page. Bei jedem Click inkrementieren; per Page-URL lesen.
User-Preferences
Abschnitt betitelt „User-Preferences“LWWMap<UserId, UserPrefs>; // UserPrefs ist deine Pref-FormEin-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.
Shopping-Cart
Abschnitt betitelt „Shopping-Cart“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).
Aktive Session-Anzahl
Abschnitt betitelt „Aktive Session-Anzahl“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.
Was CRDTs nicht können
Abschnitt betitelt „Was CRDTs nicht können“Wann du DistributedData gar nicht nutzt
Abschnitt betitelt „Wann du DistributedData gar nicht nutzt“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.
Kompositions-Muster
Abschnitt betitelt „Kompositions-Muster“Echte Apps kombinieren oft CRDTs:
// Konfiguration pro Tenant:// - tenantId → { features, quota }// - features ist ein Set// - quota ist LWWtype 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 → featuresconst quotas = LWWMap.empty<string, number>(); // tenantId → quotaTrade-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).
Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Distributed Data im Überblick — das Gesamtbild.
- Counter — Detail pro CRDT.
- Register — Detail pro CRDT.
- Sets — Detail pro CRDT.
- Maps — Detail pro CRDT.
- Sharding im Überblick — die Per-Key-Actor-Alternative.
- PersistentActor — die Event-sourced Alternative.