Redis oder Memcache? Eine Architekturentscheidung für Caching-Layer
Die Architekturentscheidung: Welcher Cache-Typ sitzt vor deiner Datenbank?
Einleitung
Du baust einen Service, die Datenbank wird bei steigendem Traffic langsam, und du fragst dich: Redis oder Memcache? Beide sitzen im RAM, beide entlasten den Backend-Speicher, beide lassen sich in wenigen Minuten einrichten. Aber sie unterscheiden sich in Features, Persistenz und Betriebsmodell genug, dass die Wahl mehr als ein Benchmark-Ergebnis ist. Ich habe die Entscheidung kürzlich für ein API-Gateway mit hohem Read-Anteil treffen müssen und möchte die Kriterien teilen.
Kontext
Es geht um einen API-Proxy, der hauptsächlich GET-Anfragen an eine relationale Datenbank weiterleitet. Der Datensatz ist wiederholbar, aber nicht triviale Keys — also kein einfaches Key-Value-Pärchen, sondern teilweise strukturierte Antworten. Das Team besteht aus zwei Entwicklern, die Redis bereits kennen, aber keine langjährige Erfahrung mit verteilten Caches haben. Infrastrukturseitig läuft alles auf Docker, Persistenz ist nicht kritisch, aber Invalidation muss zuverlässig funktionieren.
Was auf dem Spiel steht: Antwortzeiten unter 50 ms bei Spitzenlast, einfache Bedienbarkeit für Junior-Entwickler und geringer Wartungsaufwand. Non-Goals: Persistente Speicherung im Cache, komplexe Pub/Sub-Szenarien oder Multi-Datacenter-Replikation.
Option A: Memcache
Memcache ist der Oldie: einfach, schnell, für genau einen Zweck gemacht.
Pros:
- Extrem leichtgewichtig — RAM-Overhead pro Eintrag ist minimal
- Multi-Threaded, skaliert gut über CPU-Kerne hinweg
- Keine Persistenz, kein Datenverlust nach Restart — das ist hier gewollt
Cons:
- Keine Datenstrukturen außer Strings; du musst serialisieren und deserialisieren
- Keine nativen Invalidation-Strategien außer TTL oder manuellem
delete - Kein Cluster-Modus — Skalierung über Partitionierung auf Anwendungsebene
# Beispiel: Memcache über CLI oder libmemcached
memcached -m 512 -c 1024 -p 11211
# Python-Beispiel mit pymemcache
from pymemcache.client.base import Client
client = Client(('localhost', 11211))
client.set('user:123', b'{"name":"Kai"}', expire=300)
Option B: Redis
Redis positioniert sich als mehr als nur Cache — und genau das ist der Unterschied.
Pros:
- Datenstrukturen: Strings, Hashes, Lists, Sets, Streams — du kannst strukturierte Daten nativ speichern
- Flexible Invalidation durch TTL, aber auch durch Pub/Sub oder Lua-Skripte
- Optional persistence, was für Debugging nützlich sein kann
- Reifes Ökosystem, gute Clients für alle gängigen Sprachen
Cons:
- Single-Threaded: ein langsamer Command blockiert die gesamte Instanz
- Höherer Speicherverbrauch durch interne Datenstrukturen und optionale Persistenz
- Cluster-Setup ist komplexer als bei Memcache
# Beispiel: Redis mit strukturiertem Hash
HSET user:123 name "Kai" role "architect"
EXPIRE user:123 300
# Oder als JSON-String mit nativen Operationen
SET api:response:/users/123 '{"name":"Kai"}' EX 300
Entscheidung
Ich habe mich für Redis entschieden.
Die Begründung ist konkret: Unser Use Case braucht strukturierte Keys mit mehreren Feldern, und wir wollen Invalidation nicht nur über TTL steuern. Redis Hashes erlauben es uns, Felder einzeln zu aktualisieren oder zu prüfen, ohne das gesamte Objekt neu zu serialisieren. Das spart Bandbreite und reduziert Fehler bei Cache-Staleness.
Der Trade-off: Wir müssen darauf achten, dass keine langsamen Commands die Instanz blockieren. Das bedeutet: Keine KEYS *-Aufrufe in Produktion, keine großen Lua-Skripte ohne Timeout. Ich akzeptiere dieses Risiko zugunsten der besseren Datenmodellierung und der geringeren Komplexität bei Invalidation.
Implementierung
Der Einstieg mit Docker Compose:
# docker-compose.yml
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru
volumes:
redis-data:
In der Anwendung nutzen wir einen einfachen Wrapper:
// Beispiel: Go-Redis-Client
import "github.com/redis/go-redis/v9"
func GetUser(ctx context.Context, id int) (*User, error) {
key := fmt.Sprintf("user:%d", id)
val, err := rdb.Get(ctx, key).Result()
if err == nil {
return parseUser(val), nil
}
// Cache Miss → Datenbank
user := fetchFromDB(id)
rdb.Set(ctx, key, serialize(user), 5*time.Minute)
return user, nil
}
Für Invalidation nach Updates reicht ein einfaches DEL:
redis-cli DEL user:123
Fazit
Redis ist für uns die bessere Wahl, weil wir strukturierte Daten cachen und Invalidation fein steuern müssen. Memcache bleibt die richtige Entscheidung, wenn du maximalen Durchsatz bei extrem großen, flachen Datensätzen brauchst und dir keine Datenstrukturen wünschst.
Die Metrik, die für uns zählt: Die Cache-Trefferquote liegt bei über 94 Prozent, die durchschnittliche Antwortzeit des API-Gateways ging von 120 ms auf 22 ms runter. Das ist der Unterschied, den Operations und Nutzer gleichermaßen spüren.
Wie validiert ihr Cache-Invalidation in euren Systemen? Ich bin neugierig, ob jemand mit gemischten Ansätzen — also etwa Redis für strukturierte Daten und Memcache für flache Hot-Keys — Erfahrung hat.
Siehe auch
[Nginx oder Caddy? Eine Architekturentscheidung](/blog/nginx-oder-caddy-eine-architekturentscheidung/)[Nginx Rate Limiting gegen Brute Force](/blog/nginx-rate-limiting-brute-force-schutz/)