Cache im Überblick
Das Cache-Interface ist der opportunistische Key/Value-Cache
des Frameworks. Sieben Operationen decken ~95 % der realen Fälle ab:
get, set, atomares Inkrement, set-if-absent, delete, Bulk-Get,
Bulk-Set.
Genutzt von:
- Response-Cache-Middleware — Caching von GET-Responses.
- Rate-Limit-Middleware — Zähler pro Key.
- Idempotency-Key-Middleware — Write-Dedup.
- CachedSnapshotStore — Read-Through-Cache für Snapshots.
Das Interface
Abschnitt betitelt „Das Interface“interface Cache { get<V>(key: string): Promise<Option<V>>; set<V>(key: string, value: V, ttlMs?: number): Promise<void>; incr(key: string, ttlMs?: number): Promise<number>; setIfAbsent<V>(key: string, value: V, ttlMs?: number): Promise<boolean>; delete(...keys: string[]): Promise<void>; mget<V>(keys: string[]): Promise<Map<string, V>>; mset<V>(entries: ReadonlyMap<string, V>, ttlMs?: number): Promise<void>;}Kleine Oberfläche — keine Pattern-Scans, kein Pub/Sub (der Cluster hat sein eigenes Pub/Sub).
Drei Backends
Abschnitt betitelt „Drei Backends“| Backend | Einsatz |
|---|---|
InMemoryCache | Single-Pod / Tests. In-Process-Map. |
RedisCache | Multi-Pod-Produktion. Umschließt ioredis. |
MemcachedCache | Multi-Pod, wo Memcached passt. Umschließt memjs. |
Wähle nach Deployment-Form:
- Single-Pod — InMemoryCache. Schnell, keine extra Peer-Deps.
- Multi-Pod mit bereits vorhandenem Redis — RedisCache.
- Multi-Pod mit bereits vorhandenem Memcached — MemcachedCache.
- Multi-Pod ohne Präferenz — RedisCache. Mehr Features (Pub/Sub, Persistenz usw.) und das größere Ökosystem.
Opportunistische Semantik
Abschnitt betitelt „Opportunistische Semantik“Caches sind per Design verlustbehaftet. Ein get, das
None zurückgibt, heißt “nicht gecacht” — Aufgabe des Aufrufers
ist, auf die Quelle der Wahrheit zurückzufallen:
const cached = await cache.get<User>(`user:${id}`);if (cached.isSome()) return cached.value;
const user = await db.users.findById(id); // ← Quelle der Wahrheitawait cache.set(`user:${id}`, user, 60_000);return user;Wenn set fehlschlägt (Redis down, Netzwerk-Aussetzer), bleibt
der Cache leer — aber der Aufruf liefert trotzdem die richtige
Antwort (die Quelle der Wahrheit wurde konsultiert).
Cache-Implementierungen liefern bei transienten Fehlern Defaults zurück, statt zu werfen. Missbrauch (schlechte TTL, malformed Value) wirft.
TTL-Semantik
Abschnitt betitelt „TTL-Semantik“await cache.set('key', value, 60_000); // läuft in 60s abawait cache.set('key', value); // kein Ablauf- Mit
ttlMs— Eintrag läuft nach dem Fenster ab. - Ohne — Eintrag bleibt, bis er evicted wird (LRU bei InMemoryCache; Backend-Policy bei Redis / Memcached).
Die meisten Einsätze: immer eine TTL setzen. Einträge ohne TTL wachsen bis zur Eviction; explizite TTLs sind vorhersehbar.
Atomares Inkrement
Abschnitt betitelt „Atomares Inkrement“const count = await cache.incr(`requests:${userId}`, 60_000);if (count > 100) throw new Error('rate limit');incr liefert den neuen Zählerstand nach dem Inkrement.
Wenn ttlMs gesetzt ist UND der Zähler gerade erst erzeugt wurde
(count === 1), wird die TTL gesetzt.
Wird von der Rate-Limit-Middleware für Fixed-Window-Counter genutzt.
Set-if-absent
Abschnitt betitelt „Set-if-absent“const got = await cache.setIfAbsent('lock:key', 'me', 5_000);if (got) { // Ich habe das Rennen gewonnen; mach die Arbeit} else { // Jemand anderes hat es}Atomarer Schreibvorgang im CAS-Stil. Wird von der Idempotency- Key-Middleware genutzt, um “ich bin der erste Request mit diesem Key” zu erkennen.
Atomarität ist eine Garantie, kein Best Effort. Jedes Backend
bildet das auf ein einzelnes natives Compare-and-Set ab — Redis
SET … NX, Memcached ADD, und bei InMemoryCache ein
Map-Lese/Schreib-Paar, das die single-threaded Event-Loop nicht
verschränken kann. Bei N nebenläufigen Aufrufern sieht genau
einer true. Kein Backend implementiert das als get-dann-set
— dabei gäbe es ein Fenster, in dem zwei Aufrufer den Key beide
als abwesend sehen und beide schreiben.
ttlMs gilt nur für den Schreibvorgang, der gewinnt — ein
verlierender Aufruf lässt die Ablaufzeit des vorhandenen Eintrags
unangetastet, eine Retry-Schleife kann also niemals fremden Besitz
verlängern. Die Sub-Sekunden-Genauigkeit hängt vom Backend ab:
Memcached rundet auf volle Sekunden auf, mit 1 s als Untergrenze.
setIfAbsent liefert die Acquire-Hälfte eines Locks. Bei der
Release-Hälfte gehen selbstgebaute Varianten schief, deshalb
bringt das Framework eine mit:
import { acquireLock } from 'actor-ts/cache';
const lock = await acquireLock(cache, 'lock:nightly-report', 30_000);if (lock.isNone()) return; // jemand anderes ist dran
try { await generateReport();} finally { const released = await lock.value.release(); if (!released) log.warn('report ran past its 30s lock TTL');}acquireLock schreibt ein zufälliges 128-Bit-Token als Wert, und
release() löscht den Key nur, wenn dieses Token noch dort
steht. Die naive Variante — cache.delete(key) im finally —
ist in dem Moment falsch, in dem ein Halter seine TTL überzieht:
sein Eintrag ist längst weg, jemand anderes hat einen eigenen
geschrieben, und das bedingungslose Löschen wirft den neuen
Besitzer raus, während der noch arbeitet. Genau das meldet der
false-Rückgabewert: unser Eintrag ist nicht mehr der unter dem
Key, der eben ausgeführte Abschnitt war also womöglich gar nicht
exklusiv.
ttlMs ist bewusst Pflicht. Ablauf ist der Weg zurück, wenn ein
Halter abstürzt oder hängt — ein Lock ohne TTL verklemmt für immer,
sobald ein Prozess im falschen Moment stirbt.
Bulk-Operationen
Abschnitt betitelt „Bulk-Operationen“const users = await cache.mget<User>(['user:1', 'user:2', 'user:3']);// → Map<string, User> — fehlende Keys nicht enthaltenRound-Trip-Optimierung. Entscheidend für Hydratation geteilter Entitäten nach einem Sharding-Rebalance — den State jeder aktiven Entität in einem Redis-Call statt N holen.
mset ist das Gegenstück:
await cache.mset(new Map([ ['user:1', user1], ['user:2', user2],]), 60_000);Wann den Cache NICHT nutzen
Abschnitt betitelt „Wann den Cache NICHT nutzen“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- In-Memory-Cache — das Default-Backend.
- Memcached-Cache — für Memcached-Deployments.
- Redis-Cache — für Redis- Deployments.
- Response-Cache-Middleware — der primäre Konsument.
Die Cache-API-Referenz deckt das
volle Interface ab.
