Read in English · Leer en español · Lire en français
Abschnitt 7.6 von 7.8 · Mittelstufe · 24 Min. Lesezeit
Caching-Strategien und ihre Fallstricke
Videolektion
5 Min. · auf Englisch gesprochen, mit UntertitelnNur anhören (Audio)
Transkript lesen
Caches machen Systeme schnell, und sie können Systeme auch lahmlegen. In dieser Lektion lernen Sie, wie ein Cache funktioniert, wie Sie seine Kopien aktuell genug halten und wie Sie verhindern, dass er im schlechtesten Moment versagt.
Stellen Sie sich ein belebtes Café vor. Statt für jede Bestellung Bohnen zu mahlen, hält der Barista ein kleines Glas gemahlenen Kaffee auf der Theke bereit. Die meisten Bestellungen werden in Sekunden aus dem Glas bedient. Erst wenn es leer ist, geht jemand ins Lager. Das Glas ist ein Cache. Es macht den Service schnell, und es schont das Lager. Aber es wirft Fragen auf. Wie lange kann gemahlener Kaffee stehen, bevor er schal schmeckt? Und was passiert, wenn das Glas im morgendlichen Andrang leer wird? Software-Caches werfen genau dieselben Fragen auf.
Ein Cache hält eine Kopie von Daten an einem schnelleren Ort als ihrem ursprünglichen Zuhause. Er senkt die Latenz und schützt die Datenbank, weil jeder Treffer eine Abfrage ist, die die Datenbank nie sieht. Der Preis wird mit Korrektheit bezahlt. Ein Cache ist eine zweite Kopie der Daten, und zwei Kopien können sich widersprechen. Die erste Frage ist also nicht, welchen Cache man nimmt. Sondern was veraltet sein darf, wie stark und für wen. Eine fünf Minuten alte Speisekarte ist in Ordnung. Ein fünf Minuten alter Kontostand vielleicht nicht.
Das sichere Standardmuster heißt Cache-Aside. Hier nutzt es ein Speisekarten-Service. Der Service fragt zuerst den Cache. Bei einem Treffer kommt der Wert sofort zurück. Bei einem Fehltreffer liest er die Datenbank, das System of Record, und speichert das Ergebnis mit einer Time to Live im Cache, nach der der Eintrag abläuft. Schreibvorgänge nehmen einen anderen Weg. Erst die Datenbank aktualisieren, dann den Cache-Eintrag löschen. Schreiben Sie nicht den neuen Wert in den Cache. Der nächste Lesezugriff hat einfach einen Fehltreffer und lädt den frischen Wert neu. Und verschwindet der Cache ganz, liest der Service einfach die Datenbank, nur langsamer. Deshalb fällt Cache-Aside sicher aus.
Einen Cache aktuell zu halten, ist bekanntlich schwer. Ein Bonmot, das Phil Karlton zugeschrieben wird, besagt, dass es in der Informatik nur zwei schwierige Dinge gibt: Cache-Invalidierung und das Benennen von Dingen. Und zwar aus folgendem Grund. Selbst Löschen beim Schreiben lässt eine schmale Race Condition übrig. Ein Leser verfehlt den Cache und liest aus der Datenbank einen Preis von neun Pfund. Dann pausiert er, vielleicht wegen eines langsamen Netzes. Inzwischen aktualisiert ein Schreiber den Preis auf elf Pfund und löscht den Cache-Eintrag. So weit, so gut. Dann setzt der Leser fort und speichert neun Pfund im Cache. Der veraltete Preis bleibt, bis seine Time to Live abläuft. Deshalb kombinieren gute Entwürfe beides: bei Änderungen invalidieren und eine Time to Live als Sicherheitsnetz behalten.
Der Anteil der Zugriffe, die der Cache beantwortet, ist seine Trefferquote. Angenommen, ein Speisekarten-Service erhält zwanzigtausend Lesezugriffe pro Sekunde. Der Cache antwortet in einer halben Millisekunde, die Datenbank in acht, und die Datenbank verkraftet etwa dreitausend Abfragen pro Sekunde. Ohne Cache geht jeder Lesezugriff an die Datenbank: fast das Siebenfache dessen, was sie verkraftet. Bei einer Trefferquote von achtzig Prozent erreichen immer noch viertausend Abfragen pro Sekunde die Datenbank. Das liegt über der Kapazität, also bilden sich Warteschlangen, und die Latenz steigt. Bei fünfundneunzig Prozent sind es tausend Abfragen pro Sekunde. Komfortabel. Bei neunundneunzig Prozent ändert sich die durchschnittliche Latenz kaum, aber die Datenbanklast sinkt auf ein Fünftel. Jede zusätzliche Neun an Treffern teilt die Datenbanklast etwa durch zehn. Nun startet der Cache leer neu. Alle zwanzigtausend Abfragen pro Sekunde treffen eine Datenbank, die für dreitausend ausgelegt ist, und sie bricht zusammen. Ein Cache, ohne den Sie nicht auskommen, ist eine kritische Abhängigkeit.
Eine Stampede entsteht, wenn ein beliebter Eintrag abläuft und Hunderte Anfragen im selben Augenblick einen Fehltreffer haben und alle zur Datenbank gehen. Die Abwehrmaßnahmen lassen sich gut kombinieren. Request Coalescing lässt nur eine Anfrage pro Schlüssel den Wert abrufen, während die anderen auf ihr Ergebnis warten. Gestreuter Ablauf addiert zu jeder Time to Live einen zufälligen Betrag, damit gemeinsam geschriebene Einträge nicht gemeinsam ablaufen. Stale-While-Revalidate liefert allen den abgelaufenen Wert aus, während eine Hintergrundanfrage einen frischen holt. Und einen neuen Cache anhand einer Liste heißer Schlüssel aufzuwärmen, bevor er Traffic annimmt, vermeidet die Stampede nach einem Neustart.
Zum Schluss vier häufige Fehler. Einen Cache einführen, bevor gemessen wurde: Oft ist die eigentliche Lösung ein Index oder eine bessere Abfrage. Gar keine Time to Live, sodass ein vergessener Codepfad für immer veraltete Daten hinterlässt. Die Datenbank nur für die gecachte Last dimensionieren, sodass ein Cache-Neustart die ganze Plattform lahmlegt. Und personenbezogene oder finanzielle Daten in geteilten Caches ablegen. Eine Seite mit dem Namen eines Kunden, nur unter ihrer Adresse gecacht, kann dem nächsten Besucher ausgeliefert werden.
Der vollständige Abschnitt vergleicht fünf Caching-Muster und behandelt Caching-Ebenen, HTTP-Header und Verdrängung. Als Nächstes, in Abschnitt sieben Punkt sieben, geht es um Schemaevolution. Bis gleich.
Was Sie wissen müssen
- Ein Cache tauscht eine zweite, möglicherweise veraltete Kopie der Daten gegen geringere Latenz, höheren Durchsatz und Schutz der Quelle; legen Sie zuerst fest, was veraltet sein darf, wie stark und für wen.
- Cache-Aside ist der sichere Standard; Read-Through verlagert das Laden in die Cache-Schicht; Write-Through hält den Cache aktuell, auf Kosten langsamerer Schreibvorgänge; Write-Behind riskiert Datenverlust und darf nie Geldbeträge halten; Refresh-Ahead passt zu wenigen heißen Schlüsseln.
- Kombinieren Sie Invalidierung bei Änderungen mit einer TTL als Sicherheitsnetz; aktualisieren Sie bei Schreibvorgängen die Datenbank und löschen Sie dann den Cache-Eintrag, statt ihn zu überschreiben.
- Verhindern Sie Stampedes mit Request Coalescing, gestreutem Ablauf (Jitter), Stale-While-Revalidate, probabilistischer vorzeitiger Aktualisierung und Cache Warming.
- Die Trefferquote bestimmt die Datenbanklast weit stärker als die durchschnittliche Latenz: Jede zusätzliche Neun an Treffern teilt die Datenbankabfragen etwa durch zehn, und ein kalter Cache kann eine Datenbank überfordern, die für gecachten Traffic dimensioniert ist.
- Caches gibt es in Browsern, CDNs, Reverse Proxys, Anwendungen und Datenbanken; HTTP steuert sie mit Cache-Control, ETag und bedingten Anfragen (RFC 9111, RFC 9110).
- LRU und LFU sind die üblichen Verdrängungsstrategien; Schlüssel müssen alles enthalten, was die Antwort verändert, und personenbezogene oder finanzielle Daten gehören nicht in gemeinsam genutzte Caches.
Warum cachen, und was es kostet
Ein Cache hält eine Kopie von Daten an einem schnelleren oder näheren Ort als ihrem ursprünglichen Zuhause, damit wiederholte Anfragen nicht jedes Mal den vollen Preis zahlen. Abschnitt 0.2 hat die Idee als Tausch von Speicherplatz gegen Zeit eingeführt. Im Architekturmaßstab ist der Tausch interessanter, weil ein Cache drei Qualitäten zugleich verändert. Er senkt die Latenz (ein Redis-Zugriff im selben Rechenzentrum dauert typischerweise deutlich unter einer Millisekunde; eine Datenbankabfrage, die fünf Tabellen verknüpft, kann Dutzende Millisekunden dauern). Er erhöht den Durchsatz und schützt die Datenbank, weil jeder Treffer eine Abfrage ist, die die Datenbank nie sieht. Und er kann die Verfügbarkeit verbessern, weil ein Cache, der leicht veraltete Daten ausliefern darf, weiterarbeitet, während die Quelle langsam oder ausgefallen ist.
Der Preis wird mit Korrektheit und Komplexität bezahlt. Ein Cache ist eine zweite Kopie der Daten, und zwei Kopien können sich widersprechen. Jeder Cache verkörpert daher eine Entscheidung darüber, wie veraltet eine Antwort sein darf, und für wen. Eine fünf Minuten alte Speisekarte ist akzeptabel; ein fünf Minuten alter Kontostand, der einem Kunden angezeigt wird, der entscheidet, ob er sich eine Zahlung leisten kann, ist es nicht. Caches fügen außerdem eine Komponente hinzu, die ausfallen kann, verbrauchen Speicher, der Geld kostet, und schaffen neue Fehlerarten wie Stampedes und Kaltstarts, die es ohne sie nicht gäbe. Die erste Frage lautet daher nicht Welcher Cache?
, sondern Was darf veraltet sein, wie stark, und was passiert, wenn es das ist?
Die Wirksamkeit eines Cache wird an seiner Trefferquote gemessen: Treffer geteilt durch alle Zugriffe. Die Trefferquote hängt davon ab, wie ungleich das Zugriffsmuster verteilt ist. Der Lesetraffic der meisten Endkundensysteme ist stark ungleich verteilt (ein kleiner Teil der Restaurants, Produkte oder Konten erhält die meisten Lesezugriffe), sodass ein Cache, der wenige Prozent der Daten hält, oft die meisten Lesezugriffe beantworten kann. Gleichmäßig zufällige Zugriffe dagegen lassen sich schlecht cachen, was man auch tut.
Die fünf klassischen Muster
Caching-Muster unterscheiden sich darin, wer den Cache befüllt, wann Schreibvorgänge die Quelle erreichen und folglich wie veraltet und wie dauerhaft die Daten sein können. Die Tabelle vergleicht sie; die Absätze danach erklären, wo jedes passt.
Caching-Muster im Vergleich
| Muster | Wie Lesen funktioniert | Wie Schreiben funktioniert | Stärken | Risiken |
|---|---|---|---|---|
| Cache-Aside (Lazy Loading) | Die Anwendung prüft den Cache; bei einem Fehltreffer liest sie die Datenbank und speichert das Ergebnis | Die Anwendung schreibt in die Datenbank und löscht (oder aktualisiert) dann den Cache-Eintrag | Einfach, cacht nur, was genutzt wird, ein Cache-Ausfall führt nur zu langsameren Lesezugriffen | Erster Lesezugriff nach Ablauf ist langsam; Race Conditions können veraltete Einträge hinterlassen; Stampedes |
| Read-Through | Die Anwendung fragt den Cache; die Cache-Bibliothek selbst lädt bei einem Fehltreffer aus der Datenbank | Meist kombiniert mit Write-Through oder Schreiben wie bei Cache-Aside | Die Ladelogik liegt an einer Stelle | Dieselben Probleme mit Veralten und Stampedes; die Cache-Schicht muss die Datenquelle kennen |
| Write-Through | Wie bei Read-Through oder Cache-Aside | Der Schreibvorgang geht synchron an Cache und Datenbank, bevor er bestätigt wird | Der Cache ist nie älter als die Datenbank, soweit Daten über ihn geschrieben werden | Jeder Schreibvorgang zahlt beide Latenzen; cacht Daten, die vielleicht nie gelesen werden |
| Write-Behind (Write-Back) | Aus dem Cache bedient | Der Schreibvorgang wird bestätigt, sobald er im Cache ist; später, oft gebündelt, in die Datenbank geschrieben | Sehr schnelle Schreibvorgänge; fängt Lastspitzen ab; bündelt Datenbankarbeit | Datenverlust, wenn der Cache vor dem Schreiben ausfällt; Datenbank hinkt hinterher; Reihenfolge und Konflikte |
| Refresh-Ahead | Aus dem Cache bedient | Betrifft keine Schreibvorgänge: heiße Einträge werden kurz vor ihrem Ablauf neu geladen | Beliebte Einträge laufen nie vor den Augen der Nutzer ab | Vergeudetes Neuladen von Einträgen, die nicht gelesen worden wären; braucht Zugriffsstatistiken |
Cache-Aside ist aus gutem Grund der Standard: Es ist leicht nachvollziehbar und fällt sicher aus, denn verschwindet der Cache, liest die Anwendung einfach die Datenbank (langsamer). Read-Through verlagert dieselbe Logik in eine Cache-Bibliothek oder ein Cache-Produkt, was aufgeräumt ist, sich im Verhalten aber nicht unterscheidet. Write-Through passt zu Daten, die kurz nach dem Schreiben gelesen werden, etwa der Sitzung oder dem Warenkorb eines Nutzers. Write-Behind ist das riskante Muster: Es macht den Cache vorübergehend zum System of Record und ist daher nur angebracht, wo der Verlust der letzten paar Sekunden an Schreibvorgängen akzeptabel ist (Aufrufzähler, Analytics-Ereignisse, Spielstände) oder wo der Cache selbst repliziert und dauerhaft ist. Verwenden Sie Write-Behind nie für Geld. Refresh-Ahead ist eine Optimierung für eine kleine Menge sehr heißer Schlüssel, etwa die Restaurantliste auf der Startseite in einer belebten Stadt.
Cache-Aside in der Praxis
import json, random
TTL = 300 # seconds
def get_menu(restaurant_id):
key = f"menu:{restaurant_id}:v3" # version the key with the data shape
cached = cache.get(key)
if cached is not None:
return json.loads(cached) # hit
with cache.lock(f"lock:{key}", timeout=5): # coalesce: one loader per key
cached = cache.get(key) # someone may have filled it meanwhile
if cached is not None:
return json.loads(cached)
menu = db.load_menu(restaurant_id) # miss: go to the system of record
jitter = random.randint(0, TTL // 10) # spread expiry times out
cache.set(key, json.dumps(menu), ex=TTL + jitter)
return menu
def update_price(restaurant_id, item_id, price):
db.update_price(item_id, price) # 1. change the system of record
cache.delete(f"menu:{restaurant_id}:v3") # 2. invalidate, do not overwriteTime to Live und Invalidierung
Es gibt zwei Wege, einen Cache daran zu hindern, geänderte Daten auszuliefern. Der erste ist eine Time to Live (TTL): Jeder Eintrag läuft nach einer festen Zeitspanne ab, was das Veralten ganz ohne Koordination begrenzt. Eine TTL von fünf Minuten für Speisekarten bedeutet, dass eine Preisänderung innerhalb von fünf Minuten überall sichtbar ist. Der zweite ist explizite Invalidierung: Ändert sich die Quelle, löscht oder aktualisiert etwas die Kopie im Cache. Invalidierung liefert frischere Daten, verlangt aber, dass jeder Codepfad, der die Daten ändert, jeden Cache kennt, der sie hält, und genau das geht schief, wenn Systeme wachsen. In verteilten Systemen wird Invalidierung oft durch Ereignisse ausgelöst: Der Speisekarten-Service veröffentlicht MenuChanged, und jeder Service, der eine Kopie hält, löscht seinen Eintrag. Die meisten professionellen Entwürfe kombinieren beides: Invalidierung bei Änderungen für Aktualität und eine TTL als Sicherheitsnetz für die Invalidierung, die verpasst wurde.
Die Schwierigkeit fasst ein Bonmot zusammen, das Phil Karlton von Netscape zugeschrieben wird: In der Informatik gibt es nur zwei schwierige Dinge: Cache-Invalidierung und das Benennen von Dingen.
Es ist schwierig, weil Invalidierung ein verkapptes Problem verteilter Systeme ist. Der Schreibvorgang in der Datenbank und das Löschen im Cache sind zwei Operationen auf zwei Systemen ohne gemeinsame Transaktion; die eine kann gelingen, während die andere scheitert, und Operationen verschiedener Anfragen können sich überlappen.
Zwei Regeln vermeiden die meisten Probleme. Erstens: Aktualisieren Sie bei einem Schreibvorgang die Datenbank und löschen Sie dann den Cache-Eintrag; schreiben Sie nicht den neuen Wert in den Cache. Zwei gleichzeitige Schreiber, die jeweils den Cache setzen, können in umgekehrter Reihenfolge zu ihren Datenbank-Commits fertig werden und den älteren Wert auf unbestimmte Zeit im Cache hinterlassen, während ein Löschen höchstens einen zusätzlichen Fehltreffer verursacht. Zweitens: Akzeptieren Sie, dass selbst Löschen beim Schreiben eine schmale Race Condition übrig lässt, wie unten gezeigt, und begrenzen Sie sie mit einer TTL. Facebooks Paper von 2013 zur Skalierung von memcache beschreibt für diesen Fall Leases: Der Cache gibt dem Client, der einen Fehltreffer hatte, ein Token und lehnt dessen späteren Schreibvorgang ab, wenn der Schlüssel in der Zwischenzeit gelöscht wurde.
Die Stale-Set-Race-Condition
Stampedes und die Thundering Herd
Eine Cache Stampede entsteht, wenn ein beliebter Eintrag abläuft oder ein Cache-Knoten leer neu startet und Hunderte gleichzeitiger Anfragen im selben Augenblick einen Fehltreffer haben. Jede geht zur Datenbank, um denselben Wert zu berechnen, und die Datenbank, dimensioniert für die normale Fehltrefferrate des Cache, wird überlastet. Langsamere Antworten bedeuten, dass sich mehr Anfragen stauen, und die Überlast kann sich kaskadenartig ausbreiten. Die verwandte Thundering Herd beschreibt viele wartende Clients, die gleichzeitig geweckt werden und um eine Ressource konkurrieren. Facebook berichtete, dass Leases bei einer Gruppe besonders anfälliger Schlüssel die Spitzenrate der Datenbankabfragen von 17.000 auf 1300 pro Sekunde senkten.
Die Abwehrmaßnahmen lassen sich gut kombinieren. Request Coalescing (auch Single Flight genannt) lässt nur eine Anfrage pro Schlüssel bei der Quelle abrufen, während die anderen auf ihr Ergebnis warten; innerhalb eines Prozesses ist das eine Map laufender Futures, prozessübergreifend eine kurze Sperre im Cache, wie im Code oben. Gestreuter Ablauf (Jitter) addiert zu jeder TTL einen zufälligen Betrag, damit gemeinsam geschriebene Einträge (nach einem Deployment oder einem Leeren des Cache) nicht gemeinsam ablaufen. Stale-While-Revalidate liefert allen den abgelaufenen Wert aus, während eine Hintergrundanfrage einen frischen holt, was die Latenzspitze vollständig beseitigt, wenn leichtes Veralten akzeptabel ist. Probabilistischer vorzeitiger Ablauf (Vattani, Chierichetti und Lowenstein, VLDB 2015) lässt jeden Leser zufällig, mit einer Wahrscheinlichkeit, die mit nahendem Ablauf steigt, über eine vorzeitige Aktualisierung entscheiden, sodass meist eine Anfrage aktualisiert, bevor der Eintrag tatsächlich abläuft. Schließlich vermeidet das Aufwärmen eines neuen Cache anhand einer Liste heißer Schlüssel, bevor er Traffic annimmt, die Kaltstart-Stampede nach einem Neustart.
Ausgearbeitetes Beispiel
Ausgearbeitetes Beispiel: was die Trefferquote mit Latenz und Datenbanklast macht
Der Speisekarten-Service der Essenslieferplattform erhält zur abendlichen Spitze 20.000 Speisekarten-Lesezugriffe pro Sekunde. Ein Cache-Zugriff dauert 0,5 ms, eine Datenbankabfrage 8 ms. Bei einem Fehltreffer zahlt der Service beides (8,5 ms) und speichert das Ergebnis asynchron. Die Datenbank verkraftet etwa 3000 solcher Abfragen pro Sekunde, bevor die Latenz steil ansteigt.
| Formel | Durchschnittliche Latenz = 0,5 + (1 − h) × 8 ms. Datenbanklast = (1 − h) × 20.000 Abfragen pro Sekunde. |
|---|---|
| Gar kein Cache | 8 ms im Durchschnitt; 20.000 Abfragen pro Sekunde, fast das Siebenfache dessen, was die Datenbank verkraftet. |
| Trefferquote 80 % | 0,5 + 0,2 × 8 = 2,1 ms; 4000 Abfragen pro Sekunde: immer noch über der Kapazität, also staut sich die Datenbank, und die Latenz steigt. |
| Trefferquote 95 % | 0,5 + 0,05 × 8 = 0,9 ms; 1000 Abfragen pro Sekunde: komfortabel. |
| Trefferquote 99 % | 0,5 + 0,01 × 8 = 0,58 ms; 200 Abfragen pro Sekunde. |
| Von 95 % auf 99 % | Die durchschnittliche Latenz verbessert sich nur um 0,32 ms, aber die Datenbanklast sinkt auf ein Fünftel, von 1000 auf 200 Abfragen pro Sekunde. |
| Cache-Cluster startet leer neu | Die Trefferquote fällt eine Weile auf nahezu 0 %: 20.000 Abfragen pro Sekunde treffen auf eine Datenbank, die für 3000 dimensioniert ist. Ohne Coalescing, Aufwärmen oder Rate Limiting bricht die Datenbank zusammen. |
| Perzentile | Bei 95 % Treffern dauern 5 % der Anfragen 8,5 ms, also liegt p95 genau auf der Kante, und p99 beträgt 8,5 ms, nicht 0,9 ms. |
Die Trefferquote ist für die Datenbank weit wichtiger als für die durchschnittliche Latenz: Jede zusätzliche Neun an Treffern teilt die Datenbanklast etwa durch zehn.
Ein Cache, ohne den das System nicht laufen kann, ist keine Optimierung mehr; er ist eine kritische Abhängigkeit, und sein Ausfall muss eingeplant werden, so wie Abschnitt 2.5 jeden anderen einplant.
Durchschnittswerte schmeicheln Caches; wie Abschnitt 2.4 gezeigt hat, bestimmen die Fehltreffer die Tail-Latenz.
Caching-Ebenen
Caches gibt es auf jeder Ebene zwischen Nutzer und Festplatte, und eine Anfrage wird von der ersten Ebene beantwortet, die eine brauchbare Kopie hat. Der Browser führt einen privaten Cache, der über HTTP-Header gesteuert wird. Ein Content Delivery Network (CDN) hält geteilte Kopien an Edge-Standorten nahe bei den Nutzern, ideal für Bilder, Skripte und öffentliche API-Antworten wie Speisekarten. Ein Reverse Proxy am Origin (Nginx, Varnish) kann ganze Antworten für einige Sekunden cachen; das heißt Microcaching und kann eine plötzliche Lastspitze abfangen. Die Anwendung cacht berechnete Objekte, entweder prozessintern (am schnellsten, aber jede Instanz hält ihre eigene Kopie, und die Kopien widersprechen sich) oder in einem gemeinsamen Cache wie Redis oder Memcached. Die Datenbank cacht Seiten im Arbeitsspeicher (die Shared Buffers von PostgreSQL, der InnoDB-Buffer-Pool) und kann teure Ergebnisse in materialisierten Views vorberechnen.
Je weiter von den Daten entfernt, desto größer die Ersparnis pro Treffer und desto schwieriger die Invalidierung. Einen Schlüssel aus Redis können Sie in einer Millisekunde löschen; ein CDN können Sie leeren, langsamer; in den Browser eines Kunden können Sie gar nicht hineingreifen, also ist jedes max-age, das Sie senden, ein Versprechen, mit dem Sie leben müssen. Lange Browser-Lebensdauern werden daher für Dateien verwendet, deren URL sich ändert, wenn sich ihr Inhalt ändert (app.3f9a1c.js), und kurze, oder Revalidierung, für alles andere.
Wo Caches sitzen
HTTP-Caching-Header
HTTP hat ein vollständiges Caching-Modell, definiert in RFC 9111 (Juni 2022), und wer es gut nutzt, kann Last abfangen, bevor sie Ihre Server erreicht. Cache-Control: max-age=60 erlaubt jedem Cache, eine Antwort 60 Sekunden lang wiederzuverwenden; s-maxage überschreibt das für geteilte Caches wie CDNs; private verbietet geteilten Caches, sie zu speichern (verwenden Sie es für alles Persönliche); no-cache bedeutet verwirrenderweise speichern, aber vor jeder Wiederverwendung beim Server nachfragen
; und no-store bedeutet nie eine Kopie behalten
, die richtige Einstellung für Kontostände und Kontoauszüge. Die Erweiterungen stale-while-revalidate und stale-if-error (RFC 5861, 2010) wenden die obigen Ideen zu Stampedes und Verfügbarkeit auf HTTP-Ebene an.
Ist eine Kopie im Cache veraltet, muss der Client sie nicht erneut herunterladen. Ein Validator lässt ihn fragen, ob sich etwas geändert hat: Der Server sendet mit der Antwort ein ETag (eine undurchsichtige Versionskennung) oder ein Last-Modified-Datum, und der Client sendet später If-None-Match oder If-Modified-Since. Hat sich nichts geändert, antwortet der Server mit 304 Not Modified ohne Body, was Bandbreite und oft den größten Teil der Serverarbeit spart. Diese bedingten Anfragen sind in RFC 9110 definiert. Der Header Vary teilt Caches mit, welche Anfrage-Header (etwa Accept-Language) unterschiedliche Antworten erzeugen, damit eine französische Speisekarte nie auf eine englische Anfrage ausgeliefert wird.
GET /restaurants/17/menu HTTP/1.1
Host: api.example-food.test
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30
ETag: "menu-17-v42"
... 18 KB of menu JSON ...
# 100 seconds later the copy is past max-age plus the stale-while-revalidate
# window, so the browser must revalidate before using it:
GET /restaurants/17/menu HTTP/1.1
If-None-Match: "menu-17-v42"
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=30
ETag: "menu-17-v42"
# A bank balance, by contrast, must never sit in any cache:
Cache-Control: no-storeVerdrängung, Dimensionierung und Konsistenzrisiken
Ein Cache ist kleiner als die Daten dahinter, also muss er etwas verdrängen, wenn er voll ist. Least Recently Used (LRU) verdrängt den Eintrag, der am längsten unberührt ist, und funktioniert gut, wenn jüngste Nutzung künftige Nutzung vorhersagt. Least Frequently Used (LFU) verdrängt den am seltensten genutzten Eintrag und widersteht einmaligen Scans (ein nächtlicher Bericht, der jedes Restaurant einmal berührt, würde einen LRU-Cache seiner wirklich heißen Einträge entleeren). Produktive Caches nähern beides an: Redis etwa zieht eine Stichprobe weniger Schlüssel, verdrängt den besten Kandidaten darunter und bietet Strategien wie allkeys-lru, allkeys-lfu und volatile-ttl. Seine Standardstrategie noeviction lehnt Schreibvorgänge ab, wenn der Speicher voll ist, was zu Redis als Datenspeicher passt, aber nicht als Cache; legen Sie die Strategie also bewusst fest.
Über das Veralten hinaus bringen Caches subtilere Konsistenzrisiken mit sich. Ein Nutzer, der seine Lieferadresse ändert und dann die alte sieht, weil der Lesezugriff aus einem Cache kam, den eine andere Instanz noch nicht invalidiert hatte, erlebt eine gebrochene Read-your-Writes-Garantie (Abschnitt 7.4); eine gängige Lösung ist, den Cache für die eigenen jüngsten Schreibvorgänge eines Nutzers zu umgehen. Prozessinterne Caches auf vielen Instanzen widersprechen einander, sodass aufeinanderfolgende Anfragen, die an verschiedene Instanzen gehen, einen Wert scheinbar rückwärts laufen lassen. Einen Fehler oder ein leeres Ergebnis ohne kurze TTL zu cachen, macht aus einem kurzen Ausfall einen langen. Daten zu cachen, die Nutzer vermischen (eine Seite mit dem Namen des Kunden, nur unter der URL gecacht), gibt personenbezogene Daten an den nächsten Besucher preis, eine bekannte Klasse von Sicherheitsvorfällen. Und Cache-Schlüssel müssen alles enthalten, was die Antwort verändert: Mandant, Sprache, Währung, Berechtigungen und die Version des Datenformats.
Prüfen Sie Ihr Verständnis
Warum sollte ein Schreibvorgang den Cache-Eintrag löschen, statt den neuen Wert in den Cache zu schreiben?
Weil zwei gleichzeitige Schreiber ihre Cache-Aktualisierungen in umgekehrter Reihenfolge zu ihren Datenbank-Commits abschließen können, sodass der ältere Wert im Cache bleibt; ein Löschen verursacht schlimmstenfalls einen zusätzlichen Fehltreffer, der den aktuellen Wert neu lädt.
Ein Cache-Zugriff kostet 1 ms und ein Datenbank-Lesezugriff 10 ms. Wie hoch ist ungefähr die durchschnittliche Latenz bei einer Trefferquote von 90 % mit Cache-Aside?
1 + 0,1 × 10 = 2 ms (jede Anfrage zahlt den Cache-Zugriff; eine von zehn zahlt zusätzlich den Datenbank-Lesezugriff).
Was ist der Unterschied zwischen Cache-Control: no-cache und no-store?
no-cache erlaubt einem Cache, die Antwort zu speichern, verlangt aber, dass er sie vor jeder Wiederverwendung beim Server revalidiert; no-store verbietet überhaupt, eine Kopie zu behalten.
Warum kann es wichtig sein, die Trefferquote von 99 % auf 99,9 % zu erhöhen, obwohl sich die durchschnittliche Latenz kaum ändert?
Weil die Datenbanklast proportional zur Fehltrefferrate ist, die von 1 % auf 0,1 % fällt, also zehnmal weniger Abfragen, die die Datenbank erreichen.
Quellen und weiterführende Literatur
Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.
Fragen: „Unsere Restaurantsuch-API ist zu Spitzenzeiten langsam. Wie entscheide ich, ob ich einen Cache hinzufügen soll, wo er hingehört und welche TTL ich verwenden soll?“