Zum Inhalt springen
Deutsch

Cached-Snapshot-Store

CachedSnapshotStore ist ein Decorator — wickle jeden SnapshotStore ein, um einen TTL-basierten Read-Through-Cache für loadLatest hinzuzufügen, gestützt auf einen beliebigen Cache (In-Memory, Redis oder Memcached).

import {
CachedSnapshotStore,
CachedSnapshotStoreOptions,
ObjectStorageSnapshotStore,
ObjectStorageSnapshotStoreOptions,
ActorSystem,
ActorSystemOptions,
} from 'actor-ts';
const objectStorageSnapshotStoreOptions = ObjectStorageSnapshotStoreOptions.create().withBackend(backend);
const underlying = new ObjectStorageSnapshotStore(
objectStorageSnapshotStoreOptions, // S3- / Filesystem-Backend
);
const cachedSnapshotStoreOptions = CachedSnapshotStoreOptions.create()
.withCache(cache) // Backing-Cache (typischerweise Redis)
.withTtlMs(60_000);
const cached = new CachedSnapshotStore(
underlying,
cachedSnapshotStoreOptions, // optionale TTL
);
const actorSystemOptions = ActorSystemOptions.create().withPersistence({
journal: myJournal,
snapshotStore: cached,
});
const system = ActorSystem.create('app', actorSystemOptions);

Drei Muster:

  1. Langsamer zugrunde liegender Store — Object-Storage mit Multi-Hop-Netzwerklatenz, verschlüsselter State mit teurer Entschlüsselung.
  2. Häufige Actor-Churn — gesharde Entities, die ständig passivieren / neu spawnen und jeweils denselben Snapshot neu lesen.
  3. Recovery-Stürme — Full-Cluster-Restart, jeder Actor lädt seinen Snapshot auf einmal. Der Cache reduziert redundante Loads, wenn derselbe Snapshot während des Sturms abgefragt wird.

Für lokale SQLite-gestützte Snapshots (Sub-Millisekunden-Reads) fügt der Cache Overhead ohne Nutzen hinzu. Nutze ihn nur, wenn der zugrunde liegende Store messbare Read-Latenz hat.

type CachedSnapshotStoreOptionsType = {
cache: Cache; // Backing-Cache (erforderlich)
ttlMs?: number; // Cache-TTL in ms, Default 5 Minuten
keyPrefix?: string; // Key-Prefix, Default 'snap:'
};
FeldWas
cacheBacking-Cache (erforderlich) — in Produktion typischerweise Redis. Jeder Cache: In-Memory, Redis oder Memcached.
ttlMsCache-TTL in Millisekunden. Default: 5 Minuten.
keyPrefixKey-Prefix (Default 'snap:') — verhindert Kollisionen in geteilten Caches.

Der zugrunde liegende Store ist das erste Argument des Konstruktors — new CachedSnapshotStore(underlying, options) — keine Option.

  • loadLatest(persistenceId) — Read-Through mit TTL. Cache prüfen; bei Treffer zurückgeben. Bei Miss aus dem zugrunde liegenden Store laden, das Ergebnis mit ttlMs cachen, zurückgeben.
  • save(persistenceId, seq, state) — Write-Through-with-Invalidate: an den zugrunde liegenden Store delegieren, dann den Cache-Eintrag löschen. Es schreibt den neuen Snapshot bewusst nicht zurück — im Cluster können zwei Knoten beim Save konkurrieren, und ein lokaler Write würde einen veralteten Eintrag hinterlassen. Der nächste Read füllt neu.
  • loadBefore(persistenceId, seq)nicht gecacht (zu viele mögliche seq-Werte, und selten genutzt — nur bei Recovery).
  • delete(persistenceId, toSeq) — an den zugrunde liegenden Store delegieren, dann den Cache-Eintrag invalidieren.

Nach einem save ist der Cache-Eintrag invalidiert, sodass der nächste loadLatest aus dem zugrunde liegenden Store neu holt und den gerade gespeicherten Snapshot zurückgibt. Die TTL ist das Sicherheitsnetz: selbst wenn ein Knoten zwischen dem zugrunde-liegenden Write und der Cache-Invalidierung abstürzt, läuft der veraltete Eintrag innerhalb von ttlMs ab.

Für einen langsamen zugrunde liegenden Store (etwa 50 ms pro Load) macht der Cache nachfolgende Loads desselben Snapshots zu Sub-Mikrosekunden-Operationen. Ein typischer Sharded-Entity-Workload sieht nach dem Aufwärmen 80–95 % Trefferquote.

Der Cache kann über Knoten geteilt werden — stütze ihn auf Redis oder Memcached, und jeder Knoten liest aus demselben Cache, sodass ein auf einem Knoten geladener Snapshot auf allen anderen ein Treffer ist. Stütze ihn stattdessen auf einen In-Memory-Cache, behält jeder Knoten seine eigene Kopie; Misses auf einem frischen Knoten zahlen die vollen Load-Kosten.