Shopware 6 & Redis: Wenn falsche Konfiguration mehr schadet als nutzt
Redis ist in Shopware 6 das Mittel der Wahl, um Cache, Sessions und HTTP-Cache aus der Datenbank bzw. dem Dateisystem zu holen und die Performance drastisch zu steigern. In der Praxis sehen wir aber regelmäßig Setups, in denen Redis zum Flaschenhals wird — nicht, weil Redis langsam ist, sondern weil es falsch konfiguriert wurde.
Dieser Artikel zeigt die häufigsten Fehler und wie man sie vermeidet.
Mit dem Klick stimmst du zu, dass YouTube (Google) Daten lädt. Der erweiterte Datenschutzmodus ist aktiv.
Fehler 1: Eine Redis-Instanz für alles
Das ist der Klassiker. Eine einzige Redis-Instanz auf Port 6379 wird für Cache, Sessions, HTTP-Cache, Cart-Persistenz, Number-Ranges, Locks, Increment-Storage und Message-Queue verwendet.
Warum das schadet:
- Cache-Daten konkurrieren mit Session-Daten um den verfügbaren Speicher. Wenn
maxmemorygreift und die Eviction-Policy Keys räumt, können das Session- oder Warenkorb-Keys sein — Kunden fliegen aus dem Checkout oder verlieren ihren Warenkorb. - Ein unbedachtes
FLUSHALLim Debugging löscht alles auf einen Schlag — Sessions, Warenkörbe, Number-Range-States. - Unterschiedliche Datentypen brauchen unterschiedliche Persistenz- und Eviction-Strategien. Cache-Daten sind flüchtig, Warenkörbe und Number-Ranges sind es nicht.
Lösung: Mindestens zwei getrennte Redis-Instanzen — ephemeral und persistent.
Seit Shopware 6.6.8.0 werden benannte Redis-Connections direkt in der shopware.yaml unterstützt:
# config/packages/shopware.yaml
shopware:
redis:
connections:
ephemeral:
dsn: '%env(REDIS_EPHEMERAL)%'
persistent:
dsn: '%env(REDIS_PERSISTENT)%'
REDIS_EPHEMERAL=redis://redis-cache:6379
REDIS_PERSISTENT=redis://redis-session:6380
Diese Connection-Namen können dann in allen Subsystemen referenziert werden — Cache, Sessions, Cart, Number-Ranges, Locks, Increment-Storage.
| Instanz | Zweck | Persistenz | Eviction-Policy |
|---|---|---|---|
ephemeral | App-Cache, HTTP-Cache, Locks, Increment | Aus | allkeys-lru |
persistent | Sessions, Warenkörbe, Number-Ranges | An (RDB + AOF) | volatile-lru |
Docker-Compose-Auszug:
services:
redis-cache:
image: redis:7-alpine
command: >
redis-server
--maxmemory 2gb
--maxmemory-policy allkeys-lru
--save ""
--appendonly no
volumes: [] # Kein Volume — Cache ist flüchtig
redis-session:
image: redis:7-alpine
command: >
redis-server
--maxmemory 256mb
--maxmemory-policy volatile-lru
--appendonly yes
--appendfsync everysec
--save "3600 1" --save "300 100"
volumes:
- redis-session-data:/data
Die Shopware-Docs empfehlen volatile-lru für die persistente Instanz — damit werden nur Keys mit gesetztem TTL
evictet, während Keys ohne Ablauf (z. B. Number-Ranges) immer erhalten bleiben. Das ist der entscheidende Unterschied zu
allkeys-lru, das auch Keys ohne TTL löschen würde.
Fehler 2: Persistenz auf der Cache-Instanz aktiviert
Standardmäßig startet Redis mit aktivierten RDB-Snapshots (save 3600 1 300 100 60 10000). Das bedeutet: Redis schreibt
regelmäßig den gesamten Datensatz auf die Festplatte, auch wenn es sich nur um flüchtigen Cache handelt.
Warum das schadet:
- Der
BGSAVE-Prozess forkt den Redis-Prozess. Bei 2 GB Cache-Daten werden kurzzeitig 2 GB zusätzlicher RAM durch Copy-on-Write belegt. Auf einer VM mit 4 GB RAM kann das zum OOM-Kill führen. - Disk-I/O durch den Dump belastet das System unnötig, besonders auf Shared-Hosting oder kleinen VPS mit langsamen Platten.
- Beim Neustart lädt Redis den Dump und stellt veraltete Cache-Daten wieder her. Das klingt erstmal gut, ist aber kontraproduktiv: Der Cache enthält nach einem Deployment möglicherweise veraltete Einträge, die Shopware dann ausliefert, statt sie neu zu generieren.
Lösung:
# redis-cache.conf (ephemeral)
save ""
appendonly no
Zwei Zeilen. Kein RDB, kein AOF. Die Cache-Instanz startet leer — genau so soll es sein. Shopware baut den Cache bei Bedarf neu auf.
Für die persistente Instanz (Sessions, Carts, Number-Ranges) ist Persistenz dagegen zwingend:
# redis-session.conf (persistent)
appendonly yes
appendfsync everysec
save 3600 1
save 300 100
Fehler 3: Kein maxmemory gesetzt
Ohne maxmemory-Limit wächst Redis so lange, bis der Arbeitsspeicher des Hosts voll ist. Der Linux-OOM-Killer beendet
dann den Redis-Prozess — oder schlimmer: den MySQL-Prozess, weil der mehr RAM belegt.
Warum das schadet:
- Unkontrollierter Speicherverbrauch führt zu Systeminstabilität.
- Swap-Nutzung macht Redis um Größenordnungen langsamer. Redis und Swap vertragen sich nicht — eine Redis-Instanz, die swappt, ist langsamer als ein Cache, der direkt auf MariaDB liegt.
Lösung:
Faustregel für einen typischen Shopware-Shop:
- Cache-Instanz (ephemeral): 1–2 GB (je nach Produktanzahl und Kategorientiefe)
- Persistent-Instanz: 128–256 MB (abhängig von gleichzeitigen Nutzern und Warenkörben)
Immer explizit setzen:
maxmemory 2gb
maxmemory-policy allkeys-lru # für ephemeral
Und überwachen. Ein INFO memory-Check gehört ins Monitoring:
redis-cli -p 6379 INFO memory | grep used_memory_human
redis-cli -p 6379 INFO memory | grep maxmemory_human
Fehler 4: Falsche Eviction-Policy
Redis bietet verschiedene Eviction-Policies. Die Wahl hat direkten Einfluss auf das Verhalten des Shops.
Häufige Fehlkonfiguration:
noevictionauf der Cache-Instanz → Redis lehnt Schreiboperationen ab, sobaldmaxmemoryerreicht ist. Shopware wirft Fehler, der Shop steht.allkeys-randomstattallkeys-lru→ Redis löscht zufällige Keys statt des am längsten nicht genutzten. Häufig angefragte Cache-Einträge werden gelöscht, selten gebrauchte bleiben.allkeys-lruauf der persistenten Instanz → Redis kann Number-Range-Keys löschen, die kein TTL haben. Das führt zu doppelt vergebenen Bestellnummern.
Die richtige Zuordnung (laut Shopware-Docs):
| Instanz | Policy | Warum |
|---|---|---|
| Ephemeral (Cache) | allkeys-lru | Älteste Einträge fliegen zuerst, neue werden immer geschrieben |
| Persistent (Sessions, Carts, Number-Ranges) | volatile-lru | Nur Keys mit TTL werden evictet; Keys ohne TTL (Number-Ranges) bleiben erhalten |
Der Unterschied ist subtil, aber kritisch: volatile-lru schützt alle Keys ohne explizites TTL vor dem Löschen.
Sessions und Warenkörbe haben TTLs und können bei Speicherdruck evictet werden. Number-Ranges haben kein TTL und bleiben
sicher.
Fehler 5: Shopware-seitige Konfiguration unvollständig
Redis läuft, die Verbindung steht — aber Shopware nutzt es nicht für alle Subsysteme, weil die Konfiguration nur teilweise gemacht wurde.
Typische Fehler:
- Nur den App-Cache auf Redis, aber Session-Handler, Cart-Storage, Locks und Increment vergessen.
- Lock-DSN zeigt noch auf
flockstatt Redis → bei Horizontal-Scaling knallt es. - Increment-Storage bleibt auf MySQL → unnötige DB-Last durch Locking-Queries.
Vollständige Konfiguration mit benannten Connections (≥ 6.6.8.0):
# config/packages/shopware.yaml
shopware:
redis:
connections:
ephemeral:
dsn: '%env(REDIS_EPHEMERAL)%'
persistent:
dsn: '%env(REDIS_PERSISTENT)%'
cache:
invalidation:
delay: 0
count: 150
delay_options:
storage: redis
connection: 'ephemeral'
cart:
redis_url: '%env(REDIS_PERSISTENT)%'
number_range:
redis_url: '%env(REDIS_PERSISTENT)%'
increment:
message_queue:
type: redis
config:
url: '%env(REDIS_EPHEMERAL)%'
user_activity:
type: redis
config:
url: '%env(REDIS_EPHEMERAL)%'
# config/packages/prod/framework.yaml
framework:
cache:
default_redis_provider: '%env(REDIS_EPHEMERAL)%'
pools:
cache.object:
adapter: cache.adapter.redis
tags: true
cache.http:
adapter: cache.adapter.redis
tags: true
session:
handler_id: '%env(REDIS_PERSISTENT)%'
lock:
main: '%env(REDIS_EPHEMERAL)%'
Wer den Increment-Storage nicht braucht (keine Nutzung der Admin-Live-Statistiken), kann ihn auf array setzen und
spart sich die Last komplett:
shopware:
increment:
message_queue:
type: array
user_activity:
type: array
Fehler 6: ?persistent=1 falsch verstanden
Ein Klassiker, den man in vielen Tutorials sieht:
shopware:
cart:
redis_url: 'redis://redis:6380?persistent=1'
Der Parameter ?persistent=1 hat nichts mit der Datenpersistenz zu tun. Er aktiviert Persistent Connections —
also Connection Pooling auf PHP-Seite. Damit wird die TCP-Verbindung zu Redis über mehrere Requests hinweg offen
gehalten, statt sie bei jedem Request neu aufzubauen.
Warum die Verwechslung schadet:
- Entwickler setzen
?persistent=1und glauben, damit sei die Datenpersistenz geregelt. Die eigentliche Redis-Persistenz (RDB/AOF) wird nicht konfiguriert. Nach einem Redis-Neustart sind alle Warenkörbe weg. - Umgekehrt:
?persistent=1wird weggelassen, weil man keine Persistenz will (Cache-Instanz). Dabei hätte Connection Pooling dort die beste Wirkung, weil Cache-Requests häufig sind.
Lösung:
- Datenpersistenz wird auf Redis-Server-Seite konfiguriert (
save,appendonly) - Connection Pooling wird in der DSN konfiguriert (
?persistent=1) — sinnvoll für beide Instanzen
Fehler 7: Cart-Migration vergessen
Shopware speichert Warenkörbe standardmäßig in MySQL. Der Umstieg auf Redis erfordert eine explizite Migration, sonst sind bestehende Warenkörbe nach dem Config-Wechsel weg.
# Warenkörbe von MySQL zu Redis migrieren
bin/console cart:migrate
Das Kommando liest die Warenkörbe aus der Datenbank und schreibt sie in die konfigurierte Redis-Instanz. Es nutzt
automatisch die redis_url aus der shopware.yaml. Der Befehl ist idempotent und kann im Deployment-Script ausgeführt
werden.
Ohne diesen Schritt verlieren alle Kunden mit aktiven Warenkörben ihre Artikel nach dem Switch.
Fehler 8: Kein Monitoring
Redis läuft still vor sich hin. Niemand merkt, dass die Hit-Rate bei 12 % liegt, der Speicher zu 98 % voll ist oder die Latenz bei 50 ms statt 0,2 ms liegt.
Mindest-Monitoring:
# Hit-Rate prüfen
redis-cli -p 6379 INFO stats | grep -E "keyspace_hits|keyspace_misses"
# Auslastung
redis-cli -p 6379 INFO memory | grep used_memory_peak_human
# Verbundene Clients
redis-cli -p 6379 INFO clients | grep connected_clients
# Slow-Log
redis-cli -p 6379 SLOWLOG GET 10
Die Hit-Rate für die Cache-Instanz sollte über 80 % liegen. Liegt sie darunter, stimmt entweder das TTL nicht, die
Eviction-Policy ist falsch, oder maxmemory ist zu niedrig und Redis räumt ständig auf.
Für ein sauberes Setup: Prometheus mit dem redis_exporter und ein Grafana-Dashboard. Die Investition von 30 Minuten
Einrichtung spart Stunden Debugging.
Fehler 9: TCP-Konfiguration ignoriert
Redis und Shopware laufen auf demselben Host, kommunizieren aber über TCP statt Unix-Socket. Oder Redis läuft auf einem separaten Host, aber TCP-Keepalive und Timeout sind auf Default.
Wenn Redis auf demselben Host läuft:
Unix-Socket statt TCP spart den gesamten TCP-Stack-Overhead. Das sind bei hohem Durchsatz messbare Mikrosekunden pro Request:
# redis-cache.conf
unixsocket /var/run/redis/redis-cache.sock
unixsocketperm 770
port 0
REDIS_EPHEMERAL=redis:///var/run/redis/redis-cache.sock
Wenn Redis remote läuft:
tcp-keepalive 60
timeout 300
tcp-backlog 511
Ohne tcp-keepalive können tote Verbindungen minutenlang offen bleiben und den Connection-Pool blockieren.
Bei Nutzung eines Redis-Clusters zusätzlich in der php.ini setzen:
redis.clusters.cache_slots=1
Das überspringt den Cluster-Node-Lookup bei jeder Verbindung.
Zusammenfassung
| Fehler | Auswirkung | Fix |
|---|---|---|
| Eine Instanz für alles | Eviction löscht Sessions/Carts | Getrennte ephemeral/persistent Instanzen |
| Persistenz auf Cache | OOM-Kill, veralteter Cache nach Restart | save "", appendonly no |
Kein maxmemory | OOM-Kill, Swap-Nutzung | Explizites Limit setzen |
| Falsche Eviction-Policy | Shop-Fehler oder Datenverlust | allkeys-lru (Cache), volatile-lru (persistent) |
| Unvollständige Shopware-Config | Redis läuft, wird aber nicht genutzt | Alle Subsysteme konfigurieren |
?persistent=1 verwechselt | Keine Datenpersistenz trotz Annahme | Server-seitige Persistenz konfigurieren |
| Cart-Migration vergessen | Warenkörbe weg nach Redis-Umstieg | bin/console cart:migrate |
| Kein Monitoring | Probleme bleiben unentdeckt | Hit-Rate, Memory, Slow-Log überwachen |
| TCP statt Socket | Unnötiger Overhead | Unix-Socket bei Co-Location |
Redis ist ein mächtiges Werkzeug — aber nur, wenn die Konfiguration zum Use-Case passt. Ein falsch konfiguriertes Redis ist schlimmer als kein Redis, weil es eine zusätzliche Fehlerquelle einführt und dabei den Eindruck erweckt, die Performance-Arbeit sei bereits erledigt.