Counter (GCounter, PNCounter)
Zwei Counter-CRDTs:
| Typ | Wertebereich | Wann |
|---|---|---|
GCounter | [0, ∞) — geht nur nach oben | Page Views, Total Messages, abgeschlossene Jobs. |
PNCounter | (-∞, ∞) — beide Richtungen | Aktive Sessions, Items im Warenkorb, verfügbarer Bestand. |
Beide konvergieren bei nebenläufigen Writes, indem sie Beiträge pro Replika tracken und beim Lesen summieren.
GCounter
Abschnitt betitelt „GCounter“import { GCounter } from 'actor-ts';
let c = GCounter.empty();c = c.increment('node-a', 3);c = c.increment('node-b', 5);c.value(); // → 8Der State ist eine Map<ReplicaId, number> — jede Replika hält
ihren eigenen Beitrag, mergebar per Schlüssel-Max.
Der Vertrag:
- Inkremente müssen ≥ 0 sein. Ein negatives
deltawirft. replicaist die stabile ID jeder Replika — in DistributedData nimmdd.selfReplicaId().- Merge nimmt das Maximum pro Replika — Merges erneut anzuwenden ist idempotent.
const a = GCounter.empty().increment('a', 3);const b = GCounter.empty().increment('b', 5);
a.merge(b).value(); // → 8a.merge(b).merge(b).value(); // → 8 (idempotent)b.merge(a).value(); // → 8 (kommutativ)Nimm ihn für jeden monoton wachsenden Count — beobachtete Events insgesamt, hochgeladene Bytes insgesamt, erfolgreiche Jobs insgesamt. Falsche Form für Dinge, die runter gehen (Stornierungen, Rücknahmen).
PNCounter
Abschnitt betitelt „PNCounter“import { PNCounter } from 'actor-ts';
let c = PNCounter.empty();c = c.increment('node-a', 5);c = c.decrement('node-b', 2);c.value(); // → 3Intern zwei GCounter — einer für positive Beiträge, einer
für negative. Der Wert ist positive.value() - negative.value().
Gleicher idempotenter und kommutativer Merge. Wichtig:
- Replika-IDs werden separat für Inkremente und Dekremente getrackt. Wenn Node A inkrementiert und dann Node A dekrementiert, subtrahiert das nicht vom selben Counter — jedes geht auf seine eigene Seite.
- Der Wert kann negativ sein, wenn Dekremente Inkremente überholen.
const a = PNCounter.empty().increment('a', 5).decrement('a', 3);a.value(); // → 2 (5 - 3)
const b = PNCounter.empty().decrement('b', 10);a.merge(b).value(); // → -8 (5 - 13)Nimm ihn für alles, was in beide Richtungen geht — Items im Warenkorb, verfügbarer Bestand, Saldo nach Ausgleichen.
Mit DistributedData verwenden
Abschnitt betitelt „Mit DistributedData verwenden“import { GCounter, type DistributedData } from 'actor-ts';
const dd: DistributedData = system.extension(DistributedDataId).start(cluster);
dd.update<GCounter>( 'request-count', GCounter.empty, (c) => c.increment(dd.selfReplicaId(), 1),);
const counter = dd.get<GCounter>('request-count');console.log(counter?.value());Drei Dinge zu beachten:
dd.selfReplicaId()ist die stabile ID dieser Replika — übergib sie jedemincrement/decrement.GCounter.emptyist die Factory — DistributedData ruft sie auf, wenn der Key noch keinen Wert hat.getist lokal — gibt zurück, was die lokale Replika beobachtet hat. Für Majority Reads nimmgetAsyncmitconsistency: 'majority'.
Auswahl zwischen beiden
Abschnitt betitelt „Auswahl zwischen beiden“| Frage | Typ |
|---|---|
| Geht der Count je nach unten? | PNCounter |
| Sonst | GCounter |
GCounter ist einfacher (eine Map, kleinere
Wire-Kodierung) und sicherer (kann nicht versehentlich
dekrementieren). Nimm ihn, wann immer die Semantik “geht nur
nach oben” ist.
Für alles, wo Dekremente vorkommen, ist PNCounter die richtige
Form — aber lies genau: “Items im Warenkorb” sieht aus wie ein
GCounter (“Anzahl hinzugefügter Items”), aber du willst
Dekremente, wenn Items entfernt werden; das ist PNCounter.
Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Distributed Data im Überblick — das Gesamtbild.
- Register — LWW- / MV-Register mit einem Wert.
- Sets — GSet (Add-only) und ORSet (mit Removes).
- Maps — CRDT-Container pro Key inklusive GCounterMap.
- Daten gestalten — das richtige CRDT für dein Problem wählen.
Die API-Referenzen GCounter und
PNCounter decken die vollständige
Oberfläche ab.