Zum Hauptinhalt springen

Caching-Strategien mit LRUCache

Lernen Sie effektive Caching-Muster kennen, um die Anwendungsleistung mit LRUCache zu verbessern.

Warum LRUCache verwenden?​

LRUCache entfernt automatisch die Least Recently Used (am wenigsten kürzlich verwendeten) Elemente, wenn die Kapazität erreicht ist, was es ideal macht für:

  • API Response Caching
  • Datenbank-Abfrageergebnisse
  • Berechnete Werte
  • Session-Daten
  • Statische Assets

Grundlegendes LRU-Muster​

import { LRUCache } from '@dstoolkit/data-structures';

const cache = new LRUCache<string, any>({ capacity: 100 });

function getCachedData(key: string): any {
// Zuerst Cache prüfen
let data = cache.get(key);
if (data) {
return data; // Cache-Treffer
}

// Cache-Fehltreffer - Daten abrufen
data = fetchExpensiveData(key);
cache.put(key, data);
return data;
}

Time-To-Live (TTL) Caching​

Cache-Einträge automatisch nach einer Zeitperiode ablaufen lassen.

Session-Caching​

interface Session {
userId: string;
loginTime: number;
data: Record<string, unknown>;
}

const sessionCache = new LRUCache<string, Session>({
capacity: 1000,
ttl: 3600000, // 1 Stunde in Millisekunden
});

function createSession(sessionId: string, userId: string): Session {
const session: Session = {
userId,
loginTime: Date.now(),
data: {},
};

sessionCache.put(sessionId, session);
return session;
}

function getSession(sessionId: string): Session | undefined {
return sessionCache.get(sessionId); // Gibt undefined zurück, wenn abgelaufen
}

// Nach 1 Stunde läuft die Session automatisch ab

API-Rate-Limiting mit TTL​

const rateLimiter = new LRUCache<string, number>({
capacity: 10000,
ttl: 60000, // 1 Minute
});

function checkRateLimit(userId: string, maxRequests: number): boolean {
const count = rateLimiter.get(userId) || 0;

if (count >= maxRequests) {
return false; // Rate Limit überschritten
}

rateLimiter.put(userId, count + 1);
return true;
}

// Verwendung
if (checkRateLimit('user-123', 100)) {
// Anfrage verarbeiten
} else {
// 429 Too Many Requests zurückgeben
}

Multi-Layer Caching​

Mehrere Cache-Ebenen für optimale Leistung kombinieren.

class MultiLayerCache<K, V> {
private l1Cache: LRUCache<K, V>; // Klein, schnell
private l2Cache: LRUCache<K, V>; // Größer, langsamer

constructor(l1Size: number, l2Size: number) {
this.l1Cache = new LRUCache({ capacity: l1Size });
this.l2Cache = new LRUCache({ capacity: l2Size });
}

get(key: K): V | undefined {
// Zuerst L1 prüfen
let value = this.l1Cache.get(key);
if (value !== undefined) {
return value; // L1-Treffer
}

// L2 prüfen
value = this.l2Cache.get(key);
if (value !== undefined) {
// Zu L1 befördern
this.l1Cache.put(key, value);
return value; // L2-Treffer
}

return undefined; // Cache-Fehltreffer
}

put(key: K, value: V): void {
this.l1Cache.put(key, value);
this.l2Cache.put(key, value);
}

delete(key: K): void {
this.l1Cache.delete(key);
this.l2Cache.delete(key);
}
}

// Verwendung: Schneller Cache für heiße Daten, langsamer für warme Daten
const cache = new MultiLayerCache<string, object>(50, 500);

Cache-Invalidierungs-Muster​

Manuelle Invalidierung​

const userCache = new LRUCache<number, User>({ capacity: 100 });

function updateUser(id: number, updates: Partial<User>): void {
// Datenbank aktualisieren
database.update('users', id, updates);

// Cache invalidieren
userCache.delete(id);

// Oder Cache direkt aktualisieren
const user = userCache.get(id);
if (user) {
Object.assign(user, updates);
userCache.put(id, user);
}
}

Musterbasierte Invalidierung​

class PrefixCache {
private cache = new LRUCache<string, any>({ capacity: 200 });

get(key: string): any {
return this.cache.get(key);
}

put(key: string, value: any): void {
this.cache.put(key, value);
}

invalidateByPrefix(prefix: string): void {
for (const [key] of this.cache) {
if (key.startsWith(prefix)) {
this.cache.delete(key);
}
}
}
}

const cache = new PrefixCache();
cache.put('user:1:profile', userData);
cache.put('user:1:posts', posts);
cache.put('user:2:profile', otherUser);

// Alle Cache-Einträge für Benutzer 1 invalidieren
cache.invalidateByPrefix('user:1:');

Zeitbasierte Invalidierung​

interface CachedValue<T> {
data: T;
timestamp: number;
ttl: number; // Benutzerdefinierte TTL pro Eintrag
}

class SmartCache<K, V> {
private cache = new LRUCache<K, CachedValue<V>>({ capacity: 100 });

put(key: K, value: V, ttl: number = 300000): void {
this.cache.put(key, {
data: value,
timestamp: Date.now(),
ttl,
});
}

get(key: K): V | undefined {
const cached = this.cache.get(key);
if (!cached) return undefined;

// Prüfen, ob abgelaufen
if (Date.now() - cached.timestamp > cached.ttl) {
this.cache.delete(key);
return undefined;
}

return cached.data;
}
}

const cache = new SmartCache<string, object>();
cache.put('short-lived', data1, 60000); // 1 Minute
cache.put('long-lived', data2, 3600000); // 1 Stunde

Cache-Vorwärmung​

Häufig abgerufene Daten in den Cache vorladen.

async function warmCache(): Promise<void> {
console.log('Cache wird vorgewärmt...');

// Beliebte Artikel laden
const popularIds = await database.query(
'SELECT id FROM products ORDER BY views DESC LIMIT 100',
);

for (const { id } of popularIds) {
const product = await database.get('products', id);
cache.put(id, product);
}

console.log(`Cache mit ${popularIds.length} Artikeln vorgewärmt`);
}

// Cache beim Anwendungsstart vorwärmen
await warmCache();

Cache-Überwachung​

Cache-Leistungsmetriken verfolgen.

class MonitoredCache<K, V> {
private cache: LRUCache<K, V>;
private hits = 0;
private misses = 0;

constructor(capacity: number) {
this.cache = new LRUCache({ capacity });
}

get(key: K): V | undefined {
const value = this.cache.get(key);
if (value !== undefined) {
this.hits++;
} else {
this.misses++;
}
return value;
}

put(key: K, value: V): void {
this.cache.put(key, value);
}

getStats() {
const total = this.hits + this.misses;
return {
hits: this.hits,
misses: this.misses,
hitRate: total > 0 ? (this.hits / total) * 100 : 0,
size: this.cache.size,
};
}

resetStats(): void {
this.hits = 0;
this.misses = 0;
}
}

const cache = new MonitoredCache<string, object>(100);

// Cache-Leistung überwachen
setInterval(() => {
const stats = cache.getStats();
console.log(`Cache-Statistiken: ${stats.hitRate.toFixed(2)}% Trefferquote`);
console.log(`Treffer: ${stats.hits}, Fehltreffer: ${stats.misses}`);
}, 60000); // Jede Minute

Best Practices​

1. Angemessene Kapazität wählen​

// Zu klein: häufige Entfernungen
const tooSmall = new LRUCache({ capacity: 10 });

// Zu groß: Speicherprobleme
const tooLarge = new LRUCache({ capacity: 1000000 });

// Genau richtig: basierend auf Working Set Size
const optimal = new LRUCache({ capacity: 1000 });

2. TTL für zeitkritische Daten verwenden​

// Sessions laufen ab
const sessions = new LRUCache({ capacity: 5000, ttl: 3600000 });

// Statische Daten können länger leben
const staticData = new LRUCache({ capacity: 100, ttl: 86400000 }); // 24h

3. Nur teure Operationen cachen​

// ✅ Gut: Teure Operationen cachen
async function getAggregatedReport(userId: string) {
const cached = reportCache.get(userId);
if (cached) return cached;

// Teuer: mehrere DB-Abfragen + Berechnung
const report = await generateComplexReport(userId);
reportCache.put(userId, report);
return report;
}

// ❌ Schlecht: Triviale Operationen nicht cachen
function addNumbers(a: number, b: number) {
// Dies nicht cachen!
return a + b;
}

4. Cache-Fehler elegant behandeln​

async function fetchWithCache(key: string): Promise<Data> {
try {
const cached = cache.get(key);
if (cached) return cached;
} catch (error) {
console.error('Cache-Lesefehler:', error);
// Ohne Cache fortfahren
}

const data = await fetchFromSource(key);

try {
cache.put(key, data);
} catch (error) {
console.error('Cache-Schreibfehler:', error);
// Ohne Caching fortfahren
}

return data;
}

Häufige Fallstricke​

1. Cache Stampede​

Problem: Mehrere Anfragen für dieselben nicht gecachten Daten treffen gleichzeitig auf das Backend.

Lösung: Promise-Caching verwenden

const promiseCache = new Map<string, Promise<any>>();

async function fetchWithPromiseCache(key: string): Promise<any> {
// Ergebnis-Cache prüfen
const cached = cache.get(key);
if (cached) return cached;

// Prüfen, ob bereits abgerufen wird
let promise = promiseCache.get(key);
if (promise) return promise;

// Abruf starten
promise = fetchExpensiveData(key).then((data) => {
cache.put(key, data);
promiseCache.delete(key);
return data;
});

promiseCache.set(key, promise);
return promise;
}

2. Veraltete Daten​

Problem: Cache enthält veraltete Daten nach Aktualisierungen.

Lösung: Bei Schreibvorgang invalidieren

function updateData(id: string, data: any): void {
database.update(id, data);
cache.delete(id); // Sofort invalidieren
}

Verwandte Themen​