Zum Inhalt springen
Deutsch

In-Memory-Cache

InMemoryCache ist die Default-Cache-Implementierung. Sie ist eine Map mit drei Dingen obendrauf: LRU-Eviction begrenzt durch maxEntries, fauler TTL pro Eintrag und einem optionalen Hintergrund-Sweep, der abgelaufene, nicht mehr angefasste Einträge zurückgewinnt. In-Process, keine Abhängigkeiten, geht bei Neustart verloren.

import { InMemoryCache, InMemoryCacheOptions } from 'actor-ts';
// Defaults: maxEntries 10 000, cleanupMs 60 000.
const cache = new InMemoryCache();
// Or tune it with the builder:
const cacheOptions = InMemoryCacheOptions.create()
.withMaxEntries(50_000)
.withCleanupMs(30_000);
const tuned = new InMemoryCache(cacheOptions);

Ein einfaches Objekt ist die Kurzform-Alternative — new InMemoryCache({ maxEntries: 50_000, cleanupMs: 30_000 }). Eine TTL wird weiterhin pro Aufruf übergeben (siehe TTL-Behandlung), nicht beim Konstruieren.

FeldBuilderDefaultBedeutung
maxEntrieswithMaxEntries(n)10000LRU-Schranke für gespeicherte Einträge. Infinity = unbegrenzt.
cleanupMswithCleanupMs(ms)60000Intervall des Hintergrund-Sweeps für abgelaufene Einträge (ms). 0 / Infinity schaltet den Sweep ab.

Werte werden einmalig beim Konstruieren auf den gemergten Settings validiert: maxEntries muss eine positive Ganzzahl (oder Infinity) sein und cleanupMs eine nicht-negative Zahl (oder Infinity). Ein ungültiger Wert wirft OptionsError — der Builder, ein einfaches Objekt und HOCON treffen alle dieselbe Prüfung.

Drei Szenarien:

  1. Tests — schnell, kein I/O, sauberes Teardown via close().
  2. Single-Process-Produktion — ein Prozess, kein Bedarf, Cache-State über Pods zu teilen.
  3. Dev / lokal — derselbe Code ohne Redis auf dem Laptop.

Für Multi-Process-Deployments nimm stattdessen Redis oder Memcached — sonst hielte jeder Prozess seine eigene, unabhängige Kopie.

InMemoryCache implementiert die vollständige Cache-Fläche:

await cache.set('user:1', user, 60_000); // value + optional TTL (ms)
const hit = await cache.get<User>('user:1'); // Option<User> — None on miss/expiry
await cache.setIfAbsent('lock:job', '1', 30_000); // true iff it was stored
await cache.incr('ratelimit:1.2.3.4', 60_000); // atomic ++, returns the new count
const many = await cache.mget<User>(['user:1', 'user:2']); // Map<string, User>
await cache.mset(new Map([['a', user1], ['b', user2]]), 60_000);
await cache.delete('user:1', 'user:2'); // one or many keys
await cache.close(); // stops the sweep + clears the Map

setIfAbsent ist hier aus einem strukturellen Grund atomar, nicht aus einem protokollarischen: Lesen und Schreiben liegen im selben synchronen Block, und die single-threaded Event-Loop kann keinen anderen Aufrufer dazwischenschieben. Das gilt nur innerhalb eines Prozesses — zwei Node-Prozesse haben je ihre eigene Map, ein lock:job-Key sichert zwischen ihnen also nichts ab. Für prozessübergreifendes Locking nimm acquireLock über RedisCache.

get gibt ein Option<V> zurück — None bei einem Miss oder nach Ablauf.

await cache.set('key', value, 60_000); // expires at now + 60s
await cache.set('key', value); // no TTL — lives until evicted/deleted

Expiry hat zwei Pfade. Er ist faul beim Zugriff — die Deadline eines Eintrags wird bei jedem get, mget, incr und setIfAbsent geprüft, und ein abgelaufener Eintrag wird an dieser Stelle verworfen. Ein Hintergrund-Sweep läuft dann alle cleanupMs (Default 60 s) und gewinnt abgelaufene Einträge zurück, die nie wieder angefasst werden, damit sie nicht auf einen Read wartend in der Map liegen. Setze cleanupMs auf 0 / Infinity, um den Sweep abzuschalten und dich allein auf faule Expiry zu verlassen.

incr setzt die TTL nur, wenn es den Zähler erzeugt (der Wert wird 1); spätere Inkremente erneuern sie nicht — die richtige Semantik für einen Fixed-Window-Rate-Limiter.

InMemoryCache ist LRU-begrenzt bei maxEntries (Default 10 000): das Einfügen eines neuen Keys über die Schranke hinaus evictet den am längsten nicht genutzten Eintrag. get/incr/setIfAbsent zählen als Nutzung und schieben einen Key ans zuletzt-genutzte Ende, sodass heiße Keys überleben und kalte zuerst evictet werden. Genau das verhindert, dass eine Flut eindeutiger, nie erneut gelesener Keys — etwa angreiferseitig gewählte Idempotency-Key- oder Rate-Limit-Keys — die Map unbegrenzt wachsen lässt.

Setze maxEntries: Infinity, um die Eviction ganz abzuschalten. Tu das nur, wenn du den Key-Raum kontrollierst — eine unbegrenzte Map läuft dem Prozess irgendwann in einen OOM.

import { CacheExtensionId, InMemoryCache } from 'actor-ts';
// The extension's `default` cache is an InMemoryCache out of the box:
const cache = system.extension(CacheExtensionId).cache();
// Override the default, or register a separate named cache:
system.extension(CacheExtensionId).setCache('default', new InMemoryCache());
const sessions = system.extension(CacheExtensionId).cache('sessions');

system.extension(CacheExtensionId).cache(name) löst einen Cache über seinen Namen auf — der Name default ist ein InMemoryCache, solange du ihn nicht ersetzt (via setCache, registerCache oder den HOCON-Pfad actor-ts.cache.<name>.plugin). HTTP-Middleware, Projection-Actors und dein eigener Code teilen sich dann eine konfigurierte Instanz, statt dass jeder seine eigene baut.

Der von der Extension gebaute In-Memory-Cache liest seine Defaults aus HOCON:

actor-ts.cache.in-memory {
maxEntries = 50000 # LRU cap
cleanupMs = 30000 # background sweep interval, ms (0 disables)
}

Für einen Wegwerf-Cache konstruiere einfach direkt new InMemoryCache() — keine Extension nötig.