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 maxmemory greift 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 FLUSHALL im 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.

InstanzZweckPersistenzEviction-Policy
ephemeralApp-Cache, HTTP-Cache, Locks, IncrementAusallkeys-lru
persistentSessions, Warenkörbe, Number-RangesAn (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:

  • noeviction auf der Cache-Instanz → Redis lehnt Schreiboperationen ab, sobald maxmemory erreicht ist. Shopware wirft Fehler, der Shop steht.
  • allkeys-random statt allkeys-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-lru auf 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):

InstanzPolicyWarum
Ephemeral (Cache)allkeys-lruÄlteste Einträge fliegen zuerst, neue werden immer geschrieben
Persistent (Sessions, Carts, Number-Ranges)volatile-lruNur 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 flock statt 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=1 und 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=1 wird 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

FehlerAuswirkungFix
Eine Instanz für allesEviction löscht Sessions/CartsGetrennte ephemeral/persistent Instanzen
Persistenz auf CacheOOM-Kill, veralteter Cache nach Restartsave "", appendonly no
Kein maxmemoryOOM-Kill, Swap-NutzungExplizites Limit setzen
Falsche Eviction-PolicyShop-Fehler oder Datenverlustallkeys-lru (Cache), volatile-lru (persistent)
Unvollständige Shopware-ConfigRedis läuft, wird aber nicht genutztAlle Subsysteme konfigurieren
?persistent=1 verwechseltKeine Datenpersistenz trotz AnnahmeServer-seitige Persistenz konfigurieren
Cart-Migration vergessenWarenkörbe weg nach Redis-Umstiegbin/console cart:migrate
Kein MonitoringProbleme bleiben unentdecktHit-Rate, Memory, Slow-Log überwachen
TCP statt SocketUnnötiger OverheadUnix-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.