跳转到内容
简体中文

Distributed data overview

此内容尚不支持你的语言。

Distributed data is the cluster’s eventually-consistent shared-state layer. Every node holds a local replica of every key. Updates apply locally first; gossip propagates them; conflicting concurrent updates merge automatically via the data type’s CRDT semantics.

cluster of 3 nodes — every node holds every key

node-1 replica of hits

GCounter a=10 b=4

node-2 replica of hits

GCounter a=10 b=5

node-3 replica of hits

GCounter a=10 b=5

gossip continuously merges

node-1 converged

GCounter a=10 b=5

node-2 converged

GCounter a=10 b=5

node-3 converged

GCounter a=10 b=5

The shared state is CRDT — conflict-free replicated data type. A handful of carefully-designed types (counters, sets, registers, maps) whose merge operation is commutative, associative, and idempotent: gossip can deliver updates in any order, repeat them, or drop them, and replicas still converge on the same value.

Three patterns:

  1. Cluster-wide counters and gauges — total request count, active session count, current rate limit. Every node can read and write without coordinating; the merged value is the global truth.
  2. Membership sets — “which sessions are active,” “which feature flags are enabled.” Add and remove items from any node; the set’s semantics handle concurrent adds correctly.
  3. Configuration registers — a single value that nodes write to and read from, with timestamps deciding the winner on concurrent writes.
import { ActorSystem, Cluster, ClusterOptions } from 'actor-ts';
import { DistributedDataId, GCounter } from 'actor-ts';
const system = ActorSystem.create('my-app');
const clusterOptions = ClusterOptions.create()
.withHost(host)
.withPort(port)
.withSeeds(seeds);
const cluster = await Cluster.join(system, clusterOptions);
const dd = system.extension(DistributedDataId).start(cluster);
// Increment a counter — no coordination, just merge into the local replica.
dd.update<GCounter>(
'request-count',
GCounter.empty,
(c) => c.increment(dd.selfReplicaId(), 1),
);
// Read the local view — gives you the latest known merged state.
const counter = dd.get<GCounter>('request-count');
console.log(counter?.value()); // sum across all known replicas

The local update is immediate; gossip propagates it to peers over the next few rounds. Reads are always local (cheap, no network), and read what the local replica currently knows — which converges toward the global state.

TypeWhat it isWhen
GCounterGrow-only counter. Each replica counts its own contribution; the value is the sum.Counts that only go up — total impressions, completed jobs.
PNCounterIncrement-and-decrement counter (two GCounters under the hood).Counts that go both ways — current active sessions.
GSetGrow-only set. Adds only, no removes.Append-only collections — observed event types, encountered users.
ORSetObserved-Remove set. Adds and removes; concurrent add+remove resolves to add (the add was observed).Membership sets where items come and go.
LWWRegister<T>Last-Writer-Wins register. A single value, the winner is the most recent timestamp.Single-value config — feature flag, last-known leader.
MVRegister<T>Multi-Value register. Concurrent writes are kept as a set; the caller picks.When you need to detect “two replicas wrote concurrently.”
LWWMap<K, V>A map of K to LWW values.Per-key single-value config.
ORMap<K, C>A map where the values are themselves CRDTs.Per-key counters, per-key sets.
GCounterMap<K>Convenience: a map of GCounters.Per-key click counters, per-tenant request counts.

The CRDT-type deep dives cover merge() semantics, when they fit, and when they don’t — counters, registers, sets and maps.

Reads and writes both have a 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' });
LevelWhat it means
'local' (default)Apply locally; rely on gossip to propagate. Read returns the local replica’s view.
'majority'Wait until ⌊N/2⌋+1 replicas have acked. Read merges majority responses.
'all'Wait for every up-member. Strongest consistency, highest latency.
{ from: 3 }Wait for exactly from acks (clamped to [1, N]).

Quorum doesn’t change the merge semantics — every update still merges into every replica eventually. Quorum just gives you a guarantee about when enough replicas have seen the write.

For most reads and writes, 'local' is the right default. Reach for 'majority' when:

  • You want a read to reflect a recent write you made — 'local' works only if you read on the same node you wrote to; majority reads cover the cross-node case.
  • You want a write to be “durable” before responding to a client — 'majority' ensures a node failure won’t lose it.
const unsubscribe = dd.subscribe<GCounter>('hits', (counter) => {
console.log(`hits is now ${counter.value()}`);
});
// ... later
unsubscribe();

The callback fires synchronously after every successful update or merge that changes the local value. Use it to wire DistributedData into the rest of your app — e.g., update a UI on changes, trigger follow-up logic when a counter crosses a threshold.

By default, DistributedData is in-memory only. When the whole cluster restarts (cold start), every key starts empty again.

For survive-restart semantics, use the durable variant:

import { DistributedDataOptions, InMemoryDurableStateStore } from 'actor-ts';
const store = new InMemoryDurableStateStore();
const distributedDataOptions = DistributedDataOptions.create().withDurableStore(store);
const dd = system.extension(DistributedDataId).start(
cluster,
distributedDataOptions,
);

withDurableStore takes any DurableStateStore (InMemory here; a SQL or object-storage backend in production). The durable layer persists the replica’s whole view to the store; on restart, the state is restored before joining the gossip.

See Durable storage for the configuration details.

The DistributedData API reference covers the full extension surface.