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.
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.