In den meisten Architekturdiagrammen ist ein Cache eine Box zwischen einem Service und seiner Datenbank. Diese Box verbirgt genau die Fragen, die entscheiden, ob der Cache hilft oder schadet: Wer legt Daten hinein, wer schreibt in die Datenbank, und was sieht ein Leser dazwischen?
Vier Strategien beantworten diese Fragen unterschiedlich: Cache-Aside, Read-Through, Write-Through und Write-Back. Dieser Guide geht jede mit demselben Beispiel durch, vergleicht sie, behandelt die Fehler, die in Produktion auftreten, und endet mit einem Diagramm, das du wiederverwenden kannst.
Zwei Fragen, die jeder Cache beantwortet
Jede Caching-Strategie ist ein Antwortpaar:
- Wer lädt beim Lesen Daten in den Cache? Entweder dein Anwendungscode oder eine Cache-Schicht, die weiß, wie sie die Datenbank erreicht.
- Wer schreibt in die Datenbank, und wann? Deine Anwendung, die Cache-Schicht im selben Request oder die Cache-Schicht später.
| Strategie | Lesen: wer füllt den Cache | Schreiben: wer schreibt in die Datenbank |
|---|---|---|
| Cache-Aside | Die Anwendung, nach einem Miss | Die Anwendung; danach invalidiert sie den Key |
| Read-Through | Die Cache-Schicht, nach einem Miss | Nicht durch die Strategie festgelegt; meist mit einer der nächsten beiden kombiniert |
| Write-Through | Meist mit Read-Through oder Cache-Aside kombiniert | Die Cache-Schicht, im selben Request |
| Write-Back (Write-Behind) | Meist mit Read-Through kombiniert | Die Cache-Schicht, später und oft gebündelt |
Das durchgehende Beispiel ist eine Produkt-API mit einer Postgres-Tabelle products und einem Redis-Cache. Eine Produktseite liest product:42, und ein Admin-Tool aktualisiert Preise.
Cache-Aside (Lazy Loading)
Die Anwendung spricht mit beiden Speichern. Sie fragt zuerst den Cache; bei einem Miss liest sie die Datenbank und legt das Ergebnis mit einer Time to Live (TTL) in den Cache. Die ElastiCache-Dokumentation von AWS beschreibt dasselbe Muster als Lazy Loading.
def get_product(product_id):
key = f"product:{product_id}"
cached = redis.get(key)
if cached is not None:
return json.loads(cached) # hit
row = db.fetch_one("SELECT * FROM products WHERE id = %s", product_id)
if row is not None:
redis.set(key, json.dumps(row), ex=300) # fill, with a 300 s TTL
return row
def update_price(product_id, price):
db.execute("UPDATE products SET price = %s WHERE id = %s", price, product_id)
redis.delete(f"product:{product_id}") # invalidate, don't overwrite
Lösche beim Schreiben den Key, statt den neuen Wert in den Cache zu schreiben. Der nächste Lesezugriff lädt die aktuelle Zeile. Microsofts Cache-Aside-Muster beschreibt dieselbe Schreibregel.
Was du bekommst: Nur Daten, die jemand angefragt hat, landen im Cache, und wenn der Cache ausfällt, funktionieren Lesezugriffe weiter, nur langsamer, weil sie zur Datenbank gehen. Was du zahlst: Jeder Miss kostet drei Roundtrips (Cache, Datenbank, Cache), und es gibt eine Race Condition. Ein Leser mit Miss kann die alte Zeile lesen, der Schreiber aktualisiert die Zeile und löscht den Key, und dann schreibt der Leser den alten Wert zurück. Er bleibt veraltet, bis die TTL abläuft, ein Grund, warum Cache-Aside immer eine TTL braucht.
Read-Through
Die Anwendung ruft nur den Cache auf. Bei einem Miss lädt die Cache-Schicht den Wert über einen konfigurierten Loader aus der Datenbank, speichert ihn und gibt ihn zurück. Das Leseergebnis ist dasselbe wie bei Cache-Aside, aber der Ladecode lebt an einer Stelle statt in jedem Aufrufer.
Ein normaler Redis-Server liest nicht von sich aus aus deiner Datenbank. Read-Through kommt von einer Schicht, die das kann: eine Caching-Bibliothek mit Loader-Funktion, ein Proxy oder ein Managed Cache, der für eine bestimmte Datenbank gebaut ist. Amazon DynamoDB Accelerator (DAX) ist ein Beispiel: Bei einem Cache-Miss reicht er den Lesezugriff an DynamoDB weiter, gibt das Ergebnis zurück und cacht es.
Der Kompromiss: Die Cache-Schicht liegt jetzt auf dem Lesepfad. Fällt sie aus, scheitern Lesezugriffe, sofern der Client keinen Fallback auf die Datenbank hat.
Write-Through
Die Anwendung schreibt in die Cache-Schicht, und die Schicht schreibt in derselben Operation in die Datenbank. Die Anwendung bekommt nur dann einen Erfolg, wenn beide Schreibvorgänge gelingen. DAX dokumentiert genau das: Daten werden zuerst in die DynamoDB-Tabelle geschrieben, dann in den Cache, und der Aufruf gelingt nur, wenn beide Schreibvorgänge gelingen.
Was du bekommst: Über den Cache geschriebene Daten sind im Cache aktuell, ein Lesezugriff direkt nach dem Schreiben sieht sie also. Was du zahlst: Jeder Schreibvorgang wartet auf zwei Speicher, und der Cache füllt sich mit Daten, die geschrieben, aber nie gelesen werden. Gib ihnen eine TTL, damit sie verfallen. Ein neuer, leerer Cache-Node hat außerdem nichts, bis Daten erneut geschrieben werden; deshalb wird Write-Through meist mit Read-Through oder Lazy Loading kombiniert.
Write-Back (Write-Behind)
Die Anwendung schreibt in den Cache und bekommt sofort einen Erfolg. Die Cache-Schicht schreibt später in die Datenbank, oft gebündelt, und wiederholte Updates desselben Keys fallen zu einem Datenbank-Schreibvorgang zusammen.
Was du bekommst: die niedrigste Schreiblatenz und bei Lastspitzen viel weniger Datenbank-Schreibvorgänge. Was du zahlst: Alles, was noch nicht geschrieben wurde, geht verloren, wenn der Cache-Node ausfällt, außer der Puffer wird repliziert oder durch ein dauerhaftes Log abgesichert. Außerdem hinkt die Datenbank dem Cache hinterher, sodass Reports, andere Services und Backups ältere Daten sehen. Write-Back passt zu Aufrufzählern und ähnlichen Daten, bei denen der Verlust einiger Sekunden akzeptabel ist.
Im Vergleich
| Cache-Aside | Read-Through | Write-Through | Write-Back | |
|---|---|---|---|---|
| App-Code kennt die Datenbank | Ja | Nein | Nein | Nein |
| Schreiblatenz | Datenbank-Schreibvorgang plus Löschen im Cache | Hängt von der Schreibstrategie ab | Cache und Datenbank | Nur Cache |
| Veraltete Lesezugriffe | Bis zur Invalidierung oder TTL | Bis zur Invalidierung oder TTL | Selten bei Schreibvorgängen über den Cache | Die Datenbank hinkt dem Cache hinterher |
| Cache-Node fällt aus | Langsamere Lesezugriffe | Lesezugriffe scheitern ohne Fallback | Fehlende Daten, bis neu geschrieben wird | Nicht geschriebene Änderungen gehen verloren |
| Typischer Einsatz | Der Standard mit Redis | Eine Bibliothek oder ein Managed Cache | Aktuelle Daten direkt nach dem Schreiben | Schreiblastige Zähler und Lastspitzen |
TTLs, Eviction und drei klassische Fehler
Eine TTL begrenzt, wie veraltet ein Wert werden kann. Wähle sie pro Datenart danach, wie viel Veralten das Produkt verträgt: Eine Produktbeschreibung kann minutenlang leben, ein Lagerbestand vielleicht Sekunden. Wird ein abgelaufener Key gelesen, verhält er sich, als gäbe es ihn nicht.
Eviction ist ein eigener Mechanismus: was der Cache entfernt, wenn der Speicher voll ist. In Redis setzt maxmemory die Grenze, und maxmemory-policy wählt, was entfernt wird. Der Standard in redis.conf ist noeviction; dann geben Befehle, die Daten hinzufügen, Fehler zurück, sobald der Speicher voll ist. Für einen reinen Cache empfiehlt die Redis-Dokumentation allkeys-lru als guten Standard. Managed Services setzen eigene Standards, prüf also deinen. Miss die Trefferquote aus INFO stats als keyspace_hits / (keyspace_hits + keyspace_misses).
Drei Fehler tauchen in fast jedem Cache auf:
- Stampede. Ein heißer Key läuft ab, und Hunderte Requests verfehlen ihn im selben Moment und fragen alle die Datenbank. Lass einen Request den Wert neu aufbauen und die anderen kurz warten oder den alten Wert ausliefern. Ein Redis-Lock leistet das:
SET lock:product:42 <token> NX PX 5000gelingt nur für einen Aufrufer. Facebooks Memcache-Paper löst dasselbe Problem mit Leases und berichtet, dass die Spitzenrate an Datenbankabfragen für anfällige Keys von 17.000 auf 1.300 pro Sekunde sank. - Penetration. Requests für IDs, die es nicht gibt, verfehlen den Cache immer und treffen immer die Datenbank. Cache das Ergebnis „nicht gefunden“ mit kurzer TTL, oder prüf vorher einen Bloom-Filter mit gültigen IDs.
- Avalanche. Viele Keys laufen gleichzeitig ab, etwa nach einem Bulk-Load oder einem Cache-Neustart, und die Datenbank bekommt die volle Last auf einmal. Gib den TTLs zufälligen Jitter, zum Beispiel 300 Sekunden plus bis zu 60, und wärm den Cache schrittweise auf.
Das Diagramm
Vier Zeilen mit denselben drei Beteiligten; der Unterschied steckt in den Kanten: wer mit der Datenbank spricht und wann.
Diagramminhalt
- Product API
- Redis
- Postgres
- Cache layer
- 1 GET (miss)
- 2 SELECT
- 3 SET, TTL 300 s
- 1 read
- 2 on miss: SELECT
- 1 write
- 2 UPDATE, same request
- 1 write, OK at once
- 2 flush later, batched
So liest du es:
- Zeile 1, Cache-Aside. Die Produkt-API liest Redis (1), liest bei einem Miss selbst Postgres (2) und füllt Redis mit einer TTL (3). Die Kante zu Postgres umgeht den Cache: Beide Aufrufe gehören der Anwendung.
- Zeile 2, Read-Through. Die API spricht nur mit der Cache-Schicht, die bei einem Miss aus Postgres lädt.
- Zeile 3, Write-Through. Der Schreibvorgang geht im selben Request über die Cache-Schicht nach Postgres.
- Zeile 4, Write-Back. Die API bekommt ihren Erfolg sofort. Die gestrichelte Kante ist das spätere, gebündelte Schreiben.
So zeichnest du Caches in Architekturdiagrammen
- Nenn Strategie und TTL. „Cache-Aside, TTL 300 s“ am Cache-Node oder an seiner Kante beantwortet die erste Frage jedes Reviewers.
- Zeichne, wer mit der Datenbank spricht. Bei Cache-Aside hat der Service eine Kante zur Datenbank und eine zum Cache. Ein Cache, der vor der Datenbank in der Leitung sitzt, suggeriert Read-Through, und Leser werden das annehmen.
- Zeichne den Invalidierungspfad. Zeig, wer Keys löscht, auch Batch-Jobs und Admin-Tools, die direkt in die Datenbank schreiben. Ein Schreiber ohne Pfad zum Cache ist die Quelle veralteter Daten.
- Schreib das Ausfallverhalten auf. „Cache down: Lesezugriffe gehen an Postgres“ sagt der Rufbereitschaft mehr als ein zweites Cache-Icon.
- Bring die Zahlen auf den Canvas. Ziel-Trefferquote, Speichergrenze und Eviction-Policy passen in eine kleine Notiz neben dem Cache.
- Zeichne alles Asynchrone gestrichelt. Ein Write-Back-Flush oder ein Invalidierungs-Event gehört nicht zum Request.
Die Wahl
Fang mit Cache-Aside und einer TTL an. Das funktioniert mit jedem Cache und jeder Datenbank und degradiert bei einem Cache-Ausfall zu langsameren Lesezugriffen. Wechsle zu Read-Through, wenn eine Bibliothek oder ein Managed Cache dir einen Loader gibt. Ergänze Write-Through, wenn Lesezugriffe direkt nach dem Schreiben den neuen Wert sehen müssen und Schreibvorgänge die zusätzliche Latenz vertragen. Nutze Write-Back nur, wenn du nicht geschriebene Änderungen verlieren oder erneut einspielen kannst.
Für das Bild rund um den Cache siehe das Beispiel für ein System-Design-Diagramm, in dem Redis den Lesepfad eines URL-Shorteners bedient, und die Regeln für Microservices-Diagramme, um zu zeigen, welcher Service welche Daten besitzt.
In Archboard nachbauen
- Melde dich mit Google oder GitHub an und leg ein Diagramm an.
- Füge für jede Strategie einen Text-Titel aus dem Basis-Pack hinzu und setz dann drei Nodes in eine Reihe: einen Container aus dem C4-Modell-Pack für die Produkt-API, Redis aus dem Daten-Pack für den Cache und Postgres aus dem Daten-Pack für die Datenbank. Beschrifte den Cache in den Zeilen 2 bis 4 mit „Cache layer“.
- Verbinde jede Zeile mit nummerierten Kantenbeschriftungen: „1 GET (miss)“, „2 SELECT“ und „3 SET, TTL 300 s“ für Cache-Aside und die Lese- oder Schreibschritte für die anderen Zeilen. Führ die Cache-Aside-Kante von der API zu Postgres über den Cache hinweg.
- Stell die Flush-Kante bei Write-Back gestrichelt dar.
- Füge unter jeder Zeile eine Text-Notiz aus dem Basis-Pack mit der Schreibregel hinzu und damit, was bei einem Cache-Ausfall passiert.
Das Beispiel nutzt 12 Diagramm-Nodes und bleibt damit im Free-Plan. Die allgemeine Methode hinter jedem Architekturdiagramm findest du unter Wie du ein Softwarearchitektur-Diagramm zeichnest.
FAQ
Ist Cache-Aside dasselbe wie Lazy Loading? Ja. Beide Namen beschreiben, dass die Anwendung Daten erst nach einem Miss in den Cache lädt. Die AWS-Dokumentation sagt „Lazy Loading“, viele andere Quellen sagen „Cache-Aside“.
Soll ich den Cache-Key aktualisieren oder löschen, wenn sich Daten ändern? Bei Cache-Aside löschen. Zwei Schreiber, die jeweils ihren neuen Wert in den Cache legen, können in falscher Reihenfolge fertig werden und einen alten Wert hinterlassen. Löschen plus frisches Laden vermeidet das, und die TTL begrenzt die seltenere Race Condition von oben.
Kann Redis Read-Through oder Write-Through allein? Nein. Ein Redis-Server speichert, was Clients ihm schicken. Read-Through und Write-Through brauchen eine Schicht, die deine Datenbank kennt: eine Caching-Bibliothek, einen Proxy oder einen Managed Cache für eine bestimmte Datenbank.
Welche TTL sollte ich nehmen? Die längste Veralterung, die das Produkt für diese Datenart akzeptiert. Füge dann Jitter hinzu, damit gemeinsam geladene Keys nicht gemeinsam ablaufen.
Ist Write-Back sicher? Nur, wenn der Verlust oder das erneute Einspielen nicht geschriebener Änderungen akzeptabel ist oder der Schreibpuffer repliziert oder durch ein dauerhaftes Log abgesichert ist. Vermeide es für Daten wie Zahlungen oder Bestellungen.