Zum Inhalt springen
Deutsch

Replicated Event Sourcing im Überblick

Standard-Event Sourcing hat einen Writer pro persistenceId — eine einzelne Actor-Instanz hängt Events an; eine andere Instanz (nach Failover) spielt sie ab. Das ist in Ordnung für Sharded Entities, wo das Framework Ein-Actor-pro-Key garantiert.

Replicated Event Sourcing entfernt diese Einschränkung. Mehrere Replicas derselben Entity können gleichzeitig aktiv sein — auf verschiedenen Nodes, in verschiedenen Regionen — und jede persistiert unabhängig. Das Framework verwendet Vector Clocks, um nebenläufige Edits zu erkennen + Conflict Resolver, um sie zu mergen.

persist event_A1

persist event_B1

Gossip /

asynchrone Replikation

Replica A

eu-west

Replica B

us-east

geteiltes Journal

geteiltes Journal

konvergieren über Vector Clock + Resolver

Das ist das Nischen-Persistenz-Muster. Die meisten Apps sollten es nicht brauchen. Use Cases:

  • Multi-Region Active-Active — dieselbe Entity schreibbar in EU + US. Netzwerk-Partition zwischen Regionen stoppt keine Seite.
  • Edge-artige Replikation — Entities replizieren nah am User, gleichen zentral ab.
  • Cluster-spanning gleichzeitige Writer — dieselbe Entity wird auf mehreren Cluster-Nodes ohne Singleton-Koordination bearbeitet.

Für typische Sharded-Entity-Setups gibt ClusterSharding + PersistentActor automatisch Exactly-One-Writer pro Key — einfacher als das hier.

Replicated Event Sourcing tauscht Einfachheit gegen Verfügbarkeit:

Single-Writer-ESReplicated ES
Totale Event-Order pro pidPartielle Order — nebenläufige Events können ungeordnet sein
State ist ein deterministischer FoldState ist ein Fold + Conflict Resolution
Commands werden gegen den neuesten State validiertCommands werden gegen die View der lokalen Replica validiert
Neustart spielt das Log abNeustart spielt das Log ab + gleicht nebenläufige Branches ab

Das Mental-Model ist CRDT-artig für Events — Multi-Writer- Konvergenz by Design.

import {
ReplicatedEventSourcedActor,
vectorClock,
type ConflictResolver,
} from 'actor-ts';
type State = { value: number };
type Event = { kind: 'set'; value: number };
const resolver: ConflictResolver<State, Event> = {
resolve(state, conflicts) {
// Wenn zwei Replicas gleichzeitig unterschiedliche Werte setzen, gewinnt das Maximum:
const values = conflicts.map(c => (c.event as Event).value);
return { value: Math.max(...values) };
},
};
class Counter extends ReplicatedEventSourcedActor<Cmd, Event, State> {
readonly persistenceId = 'counter-42';
readonly replicaId = process.env.REPLICA_ID!;
readonly conflictResolver = resolver;
initialState() { return { value: 0 }; }
onEvent(state: State, event: Event) {
return { value: event.value };
}
// ... onCommand etc.
}

Der Actor erweitert ReplicatedEventSourcedActor statt PersistentActor. Drei zusätzliche Dinge zu spezifizieren:

  • replicaId — der stabile Identifier dieser Replica (anders als persistenceId).
  • conflictResolver — wie nebenläufige Events gemergt werden.
  • Das Journal muss über Replicas geteilt sein (Cassandra, geteilter Object Storage, etc.).
KomponenteZweck
VectorClockVerfolgt Kausalität über Replicas — erkennt nebenläufige Writes.
ConflictResolverEntscheidet, wie nebenläufige Events in einen einzigen State gemergt werden.
Single-Writer-LeaseOptional — gattert Writes über ein Lease für stärkere Konsistenz.
Replicated SnapshotsSnapshots, die die Vector Clock für volle Recovery enthalten.

Jedes bekommt seine eigene Deep-Dive-Seite.

Sharding + PersistentActor Replicated ES
Mehrere Writer pro Entity? Nein (genau einer) Ja
Conflict Resolution nötig? Nein Ja
Cross-Region Active-Active? Sharding bevorzugt eine Region Ja
Operative Komplexität? Niedrig Hoch
Verwenden wenn Default Du brauchst es wirklich