Eine Abfrage, die 200ms gegen Ihre Datenbank braucht, braucht auch beim millionsten Aufruf noch 200ms, und jeder dieser Aufrufe konkurriert um dieselben Verbindungen, dieselbe Disk-I/O und dieselbe CPU. Redis hält die Antwort im Speicher, auf einem Server, der sonst nichts tut, sodass der millionste Lesevorgang einen Netzwerk-Roundtrip kostet statt eines Query-Plans.

Was Redis als Cache wirklich macht

Redis ist ein Key-Value-Store im Arbeitsspeicher. Werte liegen im RAM, daher liegen Lese- und Schreibzeiten im Sub-Millisekunden-Bereich statt bei den zehn bis hundert Millisekunden, die eine relationale Abfrage kostet, sobald Joins, belastete Indizes oder ein ausgelasteter Connection-Pool ins Spiel kommen. Redis steht neben Ihrer Datenbank, nicht an ihrer Stelle: Ihre Anwendung fragt zuerst Redis und geht nur dann zur Datenbank, wenn Redis die Antwort nicht hat.

Das Muster dafĂĽr heiĂźt Cache-Aside (auch “Lazy Loading”), und es ist das erste, zu dem die meisten Anwendungen greifen:

  1. Die Anwendung fragt Redis nach einem Key.
  2. Cache Hit — Redis hat ihn, gibt ihn zurück. Die Datenbank bekommt die Anfrage gar nicht zu sehen.
  3. Cache Miss — Redis hat ihn nicht. Fragt die Datenbank ab, schreibt das Ergebnis in Redis und gibt es dann zurück.
1
2
3
4
5
Request → Redis? ──Hit──→ gibt den Wert zurück
             │
            Miss
             ↓
          Datenbank → schreibt in Redis → gibt den Wert zurück

Die Datenbank bleibt die Quelle der Wahrheit. Redis hält eine schnelle, wegwerfbare Kopie der Teile, die Sie häufig lesen. Startet Redis leer neu, geht nichts verloren: Es füllt sich bei der nächsten Welle von Cache Misses von selbst wieder auf.

Wie Sie Redis mit Docker starten

Der schnellste Weg zu einer Redis-Instanz ist das offizielle Image. Wenn Sie Docker noch nie benutzt haben, deckt Was ist Docker und wofür wird es verwendet zunächst die Grundlagen ab.

1
docker run --name redis-cache -d -p 6379:6379 redis:8

Das startet Redis 8 im Hintergrund und veröffentlicht Port 6379 auf Ihrer Maschine. Verbinden Sie sich mit redis-cli, entweder lokal installiert oder aus einem zweiten Container im selben Netzwerk gestartet:

1
docker run -it --rm --network container:redis-cache redis:8 redis-cli
1
2
3
4
5
6
127.0.0.1:6379> SET user:42:name "Elena"
OK
127.0.0.1:6379> GET user:42:name
"Elena"
127.0.0.1:6379> TTL user:42:name
(integer) -1

Gibt TTL -1 zurück, hat der Key keine Ablaufzeit: Er bleibt bestehen, bis Sie ihn löschen oder Redis ihn unter Speicherdruck entfernt. Für einen Cache wollen Sie fast immer eine Ablaufzeit, damit veraltete Daten von selbst verfallen.

1
2
3
4
127.0.0.1:6379> SET user:42:name "Elena" EX 300
OK
127.0.0.1:6379> TTL user:42:name
(integer) 297

EX 300 lässt den Key in 300 Sekunden ablaufen. PX macht dasselbe in Millisekunden, und EXPIRE user:42:name 300 setzt ein TTL auf einen bereits existierenden Key.

Für alles, was über eine schnelle manuelle Prüfung hinausgeht, betreiben Sie Redis über Compose zusammen mit dem Rest Ihres Stacks — siehe Was Ist Docker Compose und Wie Wird Es Verwendet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
services:
  redis:
    image: redis:8
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    command: redis-server --save 60 1 --maxmemory 256mb --maxmemory-policy allkeys-lru

  api:
    build: .
    depends_on:
      - redis
    environment:
      - REDIS_URL=redis://redis:6379

volumes:
  redis-data:

api erreicht Redis über den Hostnamen redis, weil Compose beide Dienste in dasselbe benutzerdefinierte Netzwerk stellt — der Mechanismus aus Was ist ein Docker-Netzwerk und wie wird es verwendet. Das Volume redis-data ist für einen reinen Cache weniger wichtig als für eine primäre Datenbank (siehe Was ist ein Docker-Volume und wie wird es verwendet): Verlieren Sie den Cache, bedeutet das nur, dass die nächsten Lesevorgänge zu Cache Misses werden. Behalten Sie es trotzdem, wenn ein bereits warmer Cache nach einem Neustart für Ihr Latenzbudget zählt.

Wie Sie Cache-Aside-Logik in Ihrer Anwendung schreiben

Redis gibt Ihnen die Befehle; Ihre Anwendung muss sie trotzdem in der richtigen Reihenfolge aufrufen. Eine Datenbankabfrage in Cache-Aside einzupacken sieht so aus:

1
2
3
4
5
6
7
8
9
def get_user(user_id):
    key = f"user:{user_id}"
    cached = redis.get(key)
    if cached is not None:
        return json.loads(cached)

    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    redis.set(key, json.dumps(user), ex=300)
    return user

Zwei Details erledigen hier den GroĂźteil der Arbeit:

  • Das TTL (ex=300) ist nicht optional. Ohne es liefert eine Zeile, die sich in der Datenbank ändert, fĂĽr immer ihren alten Wert aus Redis. Richten Sie das TTL danach aus, wie lange veraltete Daten fĂĽr diesen konkreten Key akzeptabel sind, nicht nach einer pauschalen Zahl fĂĽr die ganze Anwendung.
  • json.dumps/json.loads. Redis speichert Strings, dazu eine Handvoll reichhaltigerer Typen wie Hashes und Sorted Sets — niemals die Objekte Ihrer Sprache. Alles Strukturierte muss beim Schreiben serialisiert und beim Lesen wieder geparst werden.

Ändert sich die zugrunde liegende Zeile, löschen Sie den Key, statt auf das TTL zu warten:

1
2
3
def update_user(user_id, data):
    db.execute("UPDATE users SET ... WHERE id = %s", user_id)
    redis.delete(f"user:{user_id}")

Der nächste Lesevorgang nach einem Update ist ein garantierter Cache Miss, der Redis mit der frischen Zeile wieder auffüllt. Cache-Aside versucht nie, den Cache in Echtzeit synchron zu halten. Es lässt veraltete Einträge schnell verschwinden, per TTL oder durch explizites Löschen beim Schreiben.

Eviction: Was passiert, wenn Redis der Speicher ausgeht

Redis hält alles im RAM, daher ist der Speicher begrenzt — anders, als es bei einer Festplatte meist der Fall ist. Setzen Sie mit maxmemory eine harte Obergrenze, und sagen Sie Redis mit maxmemory-policy, was zu tun ist, wenn diese Grenze erreicht ist:

PolicyVerhaltenGeeignet fĂĽr
noevictionVerweigert neue Schreibvorgänge, sobald voll, gibt einen Fehler zurückEinen Datastore, der keine Daten stillschweigend verlieren darf
allkeys-lruEntfernt den am längsten nicht genutzten Key, egal welchenEinen reinen Cache — die vernünftige Standardwahl
volatile-lruEntfernt den am längsten nicht genutzten Key unter denen mit TTLCache-Keys und dauerhafte Keys in einer Instanz mischen
volatile-ttlEntfernt zuerst den Key mit der kĂĽrzesten verbleibenden TTLRate-Limiter, kurzlebige Tokens

FĂĽr eine dedizierte Cache-Instanz ist allkeys-lru fast immer richtig: Jeder Key darin ist per Definition wegwerfbar, also lassen Sie Redis verwerfen, was Sie am längsten nicht angefasst haben. noeviction ist die falsche Policy fĂĽr einen Cache, weil sie “Redis ist voll” in einen Anwendungsfehler verwandelt, statt kalte Einträge still fallenzulassen.

1
2
CONFIG SET maxmemory 256mb
CONFIG SET maxmemory-policy allkeys-lru

Setzen Sie beides im Startbefehl, wie im Compose-File oben, nicht nur zur Laufzeit. Ein Container-Neustart setzt ein nur zur Laufzeit gesetztes CONFIG SET auf die Redis-Standardwerte zurĂĽck (maxmemory 0, also unbegrenzt, nur durch den Host gedeckelt).

Wann Redis-Caching die falsche Lösung ist

Caching versteckt einen langsamen Pfad; es repariert ihn nicht. Vier Situationen, in denen es die Sache schlimmer macht, wenn Sie zuerst zu Redis greifen:

  • Die Daten ändern sich bei jedem Lesevorgang ohnehin. Ein Live-Aktienkurs oder ein Zähler, der bei jeder Anfrage hochgezählt wird, gewinnt nichts durch einen Cache, der so schnell invalidiert wird, wie er gefĂĽllt ist. Sie haben einen Netzwerk-Hop hinzugefĂĽgt, ohne Hit-Rate zu gewinnen.
  • Sie brauchen starke Konsistenz. Cache-Aside ist per Design eventually consistent: Zwischen einem Schreibvorgang und dem nächsten Cache Miss gibt es ein Fenster, in dem noch ein veralteter Wert ausgeliefert werden kann. FĂĽr einen Kontostand oder einen Lagerbestand, bei denen dieses Fenster echten Schaden anrichtet, cachen Sie ihn nicht, oder cachen Sie ihn mit einem TTL von wenigen Sekunden und akzeptieren Sie den Kompromiss bewusst.
  • Der eigentliche Engpass liegt woanders. Ist Ihre Datenbank wegen eines fehlenden Index oder einer N+1-Abfrage langsam, beheben Sie das zuerst. Eine schlecht geschriebene Abfrage zu cachen verschiebt nur dieselbe falsche Antwort schneller in den Speicher.
  • Ein Thundering Herd beim Ablauf. Läuft ein heiĂźer Key ab, können alle gleichzeitigen Anfragen auf einmal den Cache verfehlen und die Datenbank gleichzeitig bombardieren. Nutzen Sie SET key value NX EX 30 als kurzlebige Sperre, gehalten von der Anfrage, die den Cache neu befĂĽllt, damit die ĂĽbrigen darauf warten, statt sich alle auf die Datenbank zu stĂĽrzen.

Womit Sie beim Redis-Caching anfangen sollten

Stellen Sie einen redis:8-Container vor Ihren langsamsten, am häufigsten wiederholten Lesevorgang. Packen Sie ihn in Cache-Aside mit einem TTL, das Sie begründen können, nicht mit einer zufällig gewählten runden Zahl, und setzen Sie maxmemory-policy allkeys-lru, damit ein voller Cache degradiert statt Fehler zu werfen. Messen Sie danach die Hit-Rate. Write-Through-Caching, Invalidierung per Pub/Sub und Redis Streams kosten alle Komplexität, und keines davon lohnt sich, bevor ein einfaches Cache-Aside-Setup Ihnen nicht genau gezeigt hat, wo es an seine Grenzen stößt.

Verwandte Artikel