Grundlagen des Redis-Cachings: Muster, Fallstricke und Fragen für Vorstellungsgespräche
Redis-Caching in Node.js: Cache-Aside-Strategien, TTL-Werte, Schutz vor Überlastung sowie Eviction-Strategien, zusammen mit den im Systemdesign-Interview gestellten Fragen dazu.
Quellen: Redis Isn’t Just a Cache: The Concepts Every Backend Developer Should Know
Warum brauchen wir einen Cache, wenn wir bereits eine Datenbank haben?
Stellen Sie sich einen Endpunkt in Ihrer Anwendung vor, der so aussieht:
GET /products/123
Ein Client ruft diese Route auf, und Ihr Server fragt anschließend MongoDB ab:
API → MongoDB → Product
Die Datenbank gibt das Produktprofil zurück, und alles funktioniert einwandfrei.
Nun stellen Sie sich vor, dass 10.000 Personen etwa zur gleichen Zeit nach demselben Produkt fragen.
Dann müssten Sie denselben Ablauf tausende Male wiederholen:
Request
↓
Database
↓
Same Product
Ihre Datenbank muss immer wieder dieselbe Arbeit für identische Anfragen erledigen.
wäre es nicht besser, wenn die Antwort irgendwo gespeichert werden könnte?
Dafür dient im Grunde genommen Caching.
Was ist Caching?
Ein Cache ist eine schnellere Speicherschicht, die häufig angeforderte Daten vorübergehend speichert, sodass Ihre Anwendung nicht jedes Mal auf den langsameren primären Datenspeicher zurückgreifen muss.
Anstelle des üblichen Ablaufs:
Request → Database → Response
führen wir einen Abkürzungsweg ein:
Request
↓
Cache
↓
Hit? ── Yes → Response
│
No
↓
Database
↓
Cache
↓
Response
Es geht nicht darum, die Datenbank vollständig zu beseitigen.
Sondern darum, die in der Datenbank erforderlichen Vorgänge zu reduzieren, die nicht zweimal durchgeführt werden müssen.
Cache-Hit gegen Cache-Miss
Diese beiden Begriffe tauchen ständig auf, daher lohnt es sich, sie auswendig zu kennen.
Cache-Hit
Dies tritt ein, wenn die gesuchten Daten bereits im Cache vorhanden sind.
Request
↓
Redis
↓
Found ✅
↓
Response
Cache-Miss
Dies tritt ein, wenn der Cache nichts für Sie bereithält.
Request
↓
Redis
↓
Not Found ❌
↓
Database
↓
Store in Redis
↓
Response
Eine nützliche Kennzahl hier ist der Cache-Hit-Ratio:
Cache Hit Ratio =
Cache Hits / Total Cache Requests
Ein höheres Verhältnis deutet in der Regel darauf hin, dass mehr Anfragen direkt aus dem Cache statt aus der zugrunde liegenden Datenbank beantwortet werden.
Doch eine größere Zahl bedeutet nicht automatisch Erfolg. Wenn der Cache mit veralteten oder weniger wertvollen Einträgen gefüllt ist, macht ein hohes Trefferverhältnis ihn nicht wertvoll.
Wo kommt dann Redis ins Spiel?
Redis ist ein im Speicher liegender Datenspeicher, der häufig zum Caching verwendet wird, obwohl seine Funktionen weit über diesen einen Anwendungsfall hinausgehen.
Er bietet eine Vielzahl von Datensstrukturen, darunter:
- Strings
- Hashes
- Listen
- Sets
- Gesortete Sets
- Streams
- JSON
- Sowie weitere spezialisierte Typen
Für einen einfachen Cache kann man sich das wie folgt vorstellen:
Key Value
────────────────────────────
product:123 Product data
user:42 User data
Da Redis heiße Daten im Speicher hält, können Lesevorgänge aus dem Cache äußerst schnell abgewickelt werden.
Redis in einer Node.js-Anwendung
Mit dem node-redis-Client wird er wie folgt installiert:
npm install redis
Die Einrichtung einer grundlegenden Verbindung erfordert nur ein paar Zeilen Code:
import { createClient } from "redis";
const redis = createClient();
await redis.connect();
Von dort an ist das Speichern und Abrufen von Werten einfach:
await redis.set("user:42", "Hamza");
const user = await redis.get("user:42");
Die offizielle Redis-Dokumentation weist derzeit auf node-redis als den empfohlenen Client für Node.js-Projekte hin.
Doch das Kennen von SET und GET bringt einen nur bis zu einem gewissen Punkt.
Die eigentliche Frage, die beantwortet werden muss, lautet:
Wie entwirft man eigentlich eine Anwendung um Redis herum?
Das Wichtigste Muster: Cache-Aside
Falls es ein Muster gibt, das jeder Backend-Entwickler zuerst lernen sollte, dann ist es Cache-Aside.
Dabei ist die Anwendung selbst für das Verwalten des Caches verantwortlich.
Request
↓
Check Redis
↓
┌───────────────┐
│ Cache Hit? │
└───────────────┘
↓ Yes ↓ No
Response Database
↓
Redis
↓
Response
Eine vereinfachte Version im Code sieht so aus:
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);const product = await Product.findById(id);await redis.set(key, JSON.stringify(product), {
EX: 300
});
Die Option EX: 300 setzt eine TTL von 300 Sekunden für den gespeicherten Eintrag.
Cache-Aside ist wohl das am häufigsten verwendete Caching-Muster mit Redis.
TTL: Der Cache sollte nicht ewig bestehen
Nehmen wir an, Sie haben Folgendes gecacht:
product:123
Was sollte passieren, wenn der Preis des Produkts danach aktualisiert wird?
Ohne Ablaufdatum könnten Benutzer den alten Preis unbegrenzt weiter sehen.
Deshalb gibt es das Konzept von TTL, also Time To Live.
Product stored
↓
TTL = 300 seconds
↓
Expires
↓
Next request → Database → Fresh cache
TTL setzt eine Grenze dafür, wie lange veraltete Daten bereitgestellt werden dürfen.
Trotzdem sollte man der Versuchung widerstehen, einen unnotwendig langen TTL zu wählen.
Der richtige Wert hängt vollständig davon ab, wie viel Veraltetheit Ihre Anwendung in Kauf nehmen kann.
Cache-Invalidierung: Das berühmte schwierige Problem
Betrachten Sie folgendes Szenario:
Database:
price = ₹799
Redis:
price = ₹799
Dann wird die Datenbank aktualisiert:
Database:
price = ₹899
Aber Redis behält weiterhin die alten Daten bei:
price = ₹799
Zu diesem Zeitpunkt liefert Ihre Anwendung abhängig davon, von wo aus sie liest, inkonsistente Daten.
Eine gängige Lösung ist es, den Cache jedes Mal zu löschen, wenn sich die zugrunde liegenden Daten ändern:
await Product.findByIdAndUpdate(id, update);
await redis.del(`product:${id}`);
Danach folgt der nächste Anfragenpfad diesem Weg:
Redis → Miss
↓
Database → ₹899
↓
Redis → Store fresh value
Für Systeme, die auf dem Cache-Aside-Prinzip basieren, ist es in der Regel einfacher und sicherer, bei jeder Schreiboperation einfach den betroffenen Cache-Eintrag zu löschen, anstatt versuchen zu müssen, den Cache ständig mit der Datenbank in Einklang zu halten.
Drei Cache-Probleme, die Sie kennen sollten
Das sind hervorragende Themen für Gespräche in Vorstellungsgesprächen.
1. Cache Stampede
Stellen Sie sich vor, eine häufig genutzte Schlüsselwerte läuft ab:
product:123 → EXPIRED
Kurz darauf kommen 1.000 Anfragen gleichzeitig herein.
Jede einzelne von ihnen verfehlt den Cache.
Jede einzelne von ihnen geht direkt zur Datenbank.
1000 requests
↓
1000 DB queries 💥
Der Cache sollte die Datenbank vor Last schützen, doch das Ablaufereignis selbst löste lediglich einen Anstieg der Last aus.
Typische Maßnahmen zur Prävention sind unter anderem:
- Sperrmechanismen oder sequentielle Anfragen, sodass nur eine Anfrage den Cache auffüllt, während die anderen warten
Die eigene Dokumentation von Redis behandelt ausdrücklich Strategien zum Schutz vor Überlastung bei häufig genutzten Schlüsseln.
2. Cache-Penetration
Was passiert, wenn Clients ständig nach Daten fragen, die einfach nicht existieren?
GET /users/999999
Redis gibt an, dass die Daten nicht gefunden werden.
Die Datenbank antwortet mit „nicht gefunden.“
Der nächste identische Anfrage wiederholt genau denselben Zyklus.
Ein bösartiger Akteur oder ein fehlerhafter Client könnte dies ausnutzen, um die Datenbank mit sinnlosen Abfragen zu überlasten.
Eine nützliche Gegenmaßnahme ist die negative Caching – das Speichern der Tatsache, dass etwas fehlt:
user:999999 → NOT_FOUND
TTL → short
Dadurch können wiederholte Abfragen nach derselben nicht vorhandenen Datenspur direkt aus Redis beantwortet werden, anstatt jedes Mal die Datenbank anzufordern.
3. Cache Avalanche
Stellen Sie sich nun eine große Anzahl von im Cache gespeicherten Schlüsseln vor, die alle ungefähr zur gleichen Zeit ablaufen.
Key A → expired
Key B → expired
Key C → expired
Key D → expired
↓
Massive DB traffic
Diese Situation wird als Cache Avalanche bezeichnet.
Eine gängige Methode, das Risiko zu verringern, besteht darin, großen Gruppen von Schlüsseln keine identischen Ablaufzeiten zuzuweisen – beispielsweise indem man jedem TTL einen kleinen Zufallsfaktor hinzufügt.
Cache Eviction: Was passiert, wenn Redis an Speicher kommt?
Der Speicher von Redis ist nicht unendlich.
Sobald der Speicherverbrauch die konfigurierte Obergrenze erreicht, beginnt Redis, Schlüssel gemäß der konfigurierten Löschregel zu entfernen. Redis bietet mehrere dieser Löschmodi, die es ermöglichen, die am seltensten genutzten Einträge im gesamten Schlüsselraum oder nur bei Schlüsseln mit festgelegtem Ablaufdatum zu löschen, die am seltensten genutzten Einträge in einem dieser beiden Bereiche zu löschen, auf der Grundlage der Schlüssel zu löschen, die am nächsten an ihrem Ablaufdatum sind, oder ganz neue Schreibvorgänge abzulehnen, sobald der Speicher voll ist.
LRU bedeutet im Grunde:
Beseitige alles, was schon seit längerer Zeit nicht mehr angefasst wurde.
LFU bedeutet im Grunde:
Beseitige alles, was nicht sehr oft aufgerufen wird.
Welcher Modus am besten geeignet ist, hängt von Ihren spezifischen Zugriffsmustern ab.
Redis ist nicht nur ein Cache
Dieser Punkt taucht in Interviews häufig auf.
Weil Redis mehrere eingebaute Datensstrukturen bietet, kann es weit über das Caching hinausgenutzte Zwecke erfüllen:
Strings → Caching
Hashes → Objects / sessions
Lists → Queues
Sets → Unique collections
Sorted Sets → Leaderboards
Streams → Event processing
Daher sagt die Behauptung, dass:
"Redis ist eine Caching-Datenbank"
nur einen Teil der Wahrheit wiedergibt.
Eine genaueere Beschreibung lautet:
Redis ist ein Speicher für Datensstrukturen im Arbeitsspeicher, der typischerweise zum Caching verwendet wird, aber auch zur Verwaltung von Sessions, Warteschlangen, Leaderboards, Rate-Limiting, Pub/Sub-Nachrichtenversand und vielem mehr in der Lage ist.
Redis’ offizielle Dokumentation zu Datentypen erläutert diese Strukturen sowie weitere spezialisierte Typen, die es unterstützt.
Was ist mit der Persistenz von Redis?
Da Redis im Arbeitsspeicher existiert, löscht ein Neustart alles?
Nicht unbedingt.
Redis bietet mehrere Persistenzstrategien:
- RDB – periodische Zeitpunkt-Snapshots des Datensatzes
- AOF – ein Protokoll, das jede Schreiboperation aufzeichnet
- RDB + AOF in Kombination
- Ganzekeine Persistenz
Falls Redis lediglich als Cache fungiert, ist es in der Regel kein Problem, wenn sein Inhalt beim Neustart verloren geht, da die Anwendung ihn einfach aus der primären Datenbank wieder auffüllen kann.
Aber wenn Redis selbst Daten enthält, die von Bedeutung sind, werden die Persistenzeinstellungen zu einem viel größeren Problem.
Eine häufige Interviewfalle
„Warum nicht einfach alles in Redis speichern?“
Weil der Speicher von Redis eine begrenzte und im Vergleich zu herkömmlichen persistenten Speichersystemen teure Ressource ist.
Deshalb sollte man nicht willkürlich Daten im Cache speichern.
Gute Kandidaten für das Caching weisen folgende Eigenschaften auf:
- Sie werden häufig abgerufen
- Ihre Abrufung oder Berechnung ist aufwendig
- Ein gewisser Veraltungsgrad ist tolerierbar
- Der Speicherkosten für deren Beibehaltung lohnen sich
Betrachten Sie Redis als eine Leistungssteigernde Schicht, die neben Ihrem System steht, und nicht als vollständigen Ersatz für Ihre Hauptdatenbank.
Lokales Caching vs. Verteiltes Caching
Stellen Sie sich eine Node.js-Anwendung vor, die auf drei separaten Servern läuft:
Server A → Cache A
Server B → Cache B
Server C → Cache C
Jeder dieser Server führt letztendlich sein eigenes, unabhängiges Caching durch.
Ein Benutzer könnte auf Server A zugreifen und dort einen Cache-Hit erhalten, während er später auf Server B zugreift und für genau dieselben Daten keinen Cache-Hit bekommt.
Durch Einführung einer gemeinsamen Redis-Instanz ändert sich das Bild:
┌→ Server A ─┐
Client ──────┼→ Server B ─┼→ Redis
└→ Server C ─┘
Nun zieht jede Anwendungsinstanz aus demselben gemeinsamen Cache.
Dieser Vorteil des gemeinsamen Zustands ist ein wichtiger Grund dafür, warum Redis besonders gut funktioniert, wenn eine Anwendung horizontal skaliert wird.
Das umfassende Bild des Vorstellungsgesprächs
Nehmen wir an, ein Interviewer stellt folgende Herausforderung:
"Entwerfen Sie eine Caching-Strategie für eine API eines Produkts mit hohem Traffic.
Eine schwache Antwort beschränkt sich auf:
"Ich werde Redis verwenden.
Eine stärkere Antwort erläutert die Begründung schrittweise:
Request
↓
Redis
↙ ↘
Hit Miss
↓ ↓
Response Database
↓
Redis
↓
Response
Dann werden jeweils diese Entwurfsentscheidungen behandelt:
1. Caching-Muster
Wählen Sie das Cache-Aside-Muster.
2. TTL
Legen Sie es entsprechend der Toleranz gegenüber veralteten Produktdaten fest.
3. Invalidation
Löschen Sie die entsprechende Schlüssel, sobald die zugrunde liegenden Daten aktualisiert werden.
4. Schutz vor Überlastung
Wenden Sie Verriegelungstechniken, Anfragerückführung oder Techniken zur frühzeitigen Aktualisierung an.
5. Vertreibung
Wählen Sie eine geeignete LRU- oder LFU-Strategie für die Arbeitslast aus.
6. Schlüsseldesign
product:{id}
user:{id}
7. Überwachung
Achten Sie auf:
- Hit-Ratio
- Miss-Ratio
- Latenz
- Gedächtnisnutzung
- Zahlen zu Vertreibungen
- Beliebte Schlüssel
- Redis-Fehler
Eine solche schrittweise Herangehensweise liefert eine weitaus überzeugendere Antwort zur Systemdesign-Strategie, als einfach zu behaupten, "Redis sei schnell."
Schnellüberblick zum Vorstellungsgespräch
Warum überhaupt Caching nutzen?
Es verringert die Latenz und entlastet Ihre primäre Datenspeicherung davon, immer wieder teure Lesevorgänge auszuführen.
Warum ausgerechnet Redis?
Es bietet schnellen Zugriff im Arbeitsspeicher, Unterstützung für Ablaufzeiten, vielfältige Datenstrukturen sowie Funktionen, die speziell für verteilte Systeme entwickelt wurden.
Was bedeutet „Cache-aside“?
Die Anwendung schaut zuerst im Cache nach; bei einem Fehlschlag fragt sie die Datenbank ab und schreibt das Ergebnis anschließend wieder in den Cache.
Was bedeutet TTL?
Das ist die Lebensdauer eines im Cache gespeicherten Eintrags, bevor er automatisch abläuft.
Was bedeutet Cache-Invalidierung?
Das bedeutet, dass gespeicherte Einträge gelöscht oder aktualisiert werden, sobald sich die Quelldaten ändern.
Was ist eine Cache-Stampede?
Es handelt sich um eine Situation, in der eine große Anzahl von Anfragen gleichzeitig den Cache verfehlt und alle gleichzeitig auf die Datenbank zugreifen.
LRU gegenüber LFU?
LRU entfernt den Eintrag, der zuletzt verwendet wurde. LFU entfernt den Eintrag, der insgesamt am seltensten verwendet wurde.
Muss jede Abfrage gekachtet werden?
Ganz und gar nicht. Entscheiden Sie danach, wie oft auf die Daten zugegriffen wird, wie teuer das Abrufen ist, wie viel Veraltetheit akzeptabel ist und wie viel Speicher Sie zur Verfügung haben.
Das Denkmodell, das man beibehalten sollte
Vermeiden Sie es zunächst, sich Redis-Befehle auswendig zu merken. Konzentrieren Sie sich stattdessen auf die zugrunde liegende Architektur:
┌─────────────┐
Request ─────→│ Redis │
└──────┬──────┘
│
Cache Hit?
↙ ↘
YES NO
↓ ↓
Response Database
↓
Redis
↓
Response
Dann gehen Sie vier Fragen durch:
Was gehört in den Cache?
Wie lange soll es dort bleiben?
Wann muss es ungültig gemacht werden?
Was passiert, wenn der Cache unter hoher Last versagt oder abläuft?
Durch die Bearbeitung dieser Fragen unterscheidet sich oberflächliches Wissen über Caching von produktionstauglichem Denken.
Zusammenfassung
Caching bedeutet nicht nur:
"Setzen Sie Redis vor MongoDB ein."
Eine solide Caching-Strategie dreht sich im Grunde um Abwägungen.
Ihre Abstriche lauten:
Gedächtnis gegen Geschwindigkeit
und manchmal:
Aktualität gegen Leistung
Redis stellt die Mechanismen bereit, doch es ist Ihre Anwendungslogik, die bestimmt, wie sie genutzt werden.
Falls Sie sich auf Vorstellungsgespräche vorbereiten, begnügen Sie sich nicht damit zu sagen:
"Redis ist eine In-Memory-Datenbank."
Sie sollten in der Lage sein, folgende Konzepte zu erläutern:
Cache-aside, TTL, Invalidation, Trefferquote, Eviction, Stampede-Effekt, Penetration, Avalanche-Effekt, verteiltes Caching und Konsistenz.
Sobald Sie erkennen, wie diese Konzepte miteinander verbunden sind, gehen Sie über das bloße Wissen über Redis hinaus.
Sie verstehen nun, wie Systeme mit hoher Zugriffszahl ihre Datenbanken schützen und gleichzeitig schnell bleiben.