Redis-Caching-Strategien für Magento GraphQL: granularer als Full Page Cache
AI generated
SET
TTL
Redis / Magento GraphQL
Redis-Caching-Strategien für Magento GraphQL
granularer als klassisches Full Page Caching

Magentos eingebautes Full Page Caching setzt auf eindeutige URLs und HTML-Antworten, die sich vollständig als eine Einheit cachen und über Tags invalidieren lassen. GraphQL bricht dieses Modell auf: Ein einziger POST-Endpunkt, beliebig kombinierbare Query-Strings, Variablen und teils personalisierte Felder erzeugen eine Vielzahl unterschiedlicher Antworten, die sich nicht mehr sauber über eine URL identifizieren lassen. Wer Magento headless oder als API-Backend für ein eigenes Frontend betreibt, muss deshalb eine eigene Caching-Schicht auf Query-Ebene bauen. Redis eignet sich dafür besonders gut, weil sich Query-Antworten dort granular als eigene Schlüssel ablegen, mit präzisen Tags versehen und gezielt invalidieren lassen, ohne dass gleich der gesamte Cache geleert werden muss.

12 Min. Lesezeit GraphQL Query Cache Cache-Key-Design Store-View & Customer-Group aware

1. Warum klassisches Full Page Caching bei GraphQL nicht ausreicht

Magentos Full Page Cache basiert auf dem Prinzip, dass eine URL einer eindeutigen Seite entspricht und die komplette HTML-Antwort als ein zusammenhängender Block gecacht wird. Bei GraphQL gibt es diese Eins-zu-eins-Beziehung nicht mehr: Alle Anfragen laufen über denselben /graphql-Endpunkt, und erst der Body des POST-Requests, bestehend aus Query-String und Variablen, bestimmt, welche Daten tatsächlich zurückgegeben werden. Zwei Requests an dieselbe URL können also völlig unterschiedliche Antworten liefern, wodurch eine URL-basierte Cache-Strategie ins Leere läuft.

Zusätzlich verschärfen zusammengesetzte Queries das Problem: Ein einzelner GraphQL-Request kann mehrere Resolver gleichzeitig aufrufen, etwa Produktdaten, Kategoriebaum und Kundeninformationen in einer Antwort kombinieren. Ein pauschales Cachen der Gesamtantwort würde entweder häufig personalisierte Daten fälschlich mitcachen oder, aus Vorsicht, gar nichts cachen und damit Performance-Potenzial verschenken. Genau hier setzt eine granulare, Redis-basierte Caching-Schicht auf Query-Ebene an, die einzelne Antwortteile unabhängig voneinander behandelt.

2. Der bestehende FPC-Mechanismus für cacheable GraphQL-Antworten

Magento bringt bereits einen eingebauten Mechanismus mit, der bestimmte GraphQL-Antworten über den regulären Full Page Cache abdeckt: Für Resolver, die als cacheable markiert sind und keine personalisierten Felder enthalten, setzt Magento die Header Cache-Control: public, X-Magento-Tags und einen berechneten X-Magento-Cache-Id, der Store, Währung und weitere Kontextfaktoren einbezieht. Läuft Varnish oder das eingebaute, Redis-gestützte FPC davor, wird die komplette JSON-Antwort wie eine HTML-Seite behandelt und unter diesem Cache-Key abgelegt.

Dieser Mechanismus funktioniert gut für klassische Katalogabfragen, stößt aber an Grenzen, sobald eine Query mehrere Resolver mit unterschiedlicher Cachebarkeit kombiniert oder Magento headless ohne vorgeschaltetes FPC betrieben wird, etwa wenn ein eigenes Node.js- oder React-Backend direkt gegen die GraphQL-API spricht. In solchen Fällen bleibt nur eine eigene, feingranularere Caching-Schicht, die unabhängig vom HTTP-Antwort-Caching auf Query- und Feldebene arbeitet.

3. Granulares Query-Level-Caching direkt in Redis aufbauen

Statt die gesamte HTTP-Antwort zu cachen, cacht eine query-granulare Strategie das Ergebnis einzelner, klar abgegrenzter Resolver-Aufrufe, etwa eine Produktabfrage nach SKU oder eine Kategoriebaum-Abfrage nach Kategorie-ID. Jeder dieser Aufrufe erhält einen eigenen Redis-Schlüssel, unter dem ausschließlich das serialisierte Teilergebnis liegt, nicht die komplette zusammengesetzte Antwort. Das erlaubt es, eine zusammengesetzte Query aus mehreren, unabhängig gecachten Bausteinen zusammenzusetzen, von denen einige aus dem Cache kommen und andere frisch berechnet werden.

Technisch lässt sich das als eigener Data-Loader oder als Plugin auf die relevanten Resolver-Klassen umsetzen: Vor der eigentlichen Datenbeschaffung wird zunächst im Redis-Cache nachgesehen, ob ein passender Schlüssel existiert, und nur bei einem Miss wird der teure Resolver-Code, oft inklusive mehrerer Datenbankabfragen, tatsächlich ausgeführt und das Ergebnis anschließend mit einer sinnvollen TTL zwischengespeichert.


<?php
declare(strict_types=1);

namespace Mironsoft\GraphQlCache\Plugin;

use Magento\Framework\App\CacheInterface;
use Magento\Framework\Serialize\SerializerInterface;

/**
 * Cacht das Teilergebnis eines Resolvers unabhängig von der
 * gesamten HTTP-Antwort auf Query-Ebene in Redis.
 */
class CacheResolverResult
{
    /**
     * @param CacheInterface $cache Magento-Cache-Frontend, Redis-gestützt.
     * @param SerializerInterface $serializer Zum (De-)Serialisieren des Teilergebnisses.
     */
    public function __construct(
        private readonly CacheInterface $cache,
        private readonly SerializerInterface $serializer,
    ) {
    }

    /**
     * Liest ein Teilergebnis aus dem Cache oder berechnet und speichert es neu.
     *
     * @param string $cacheKey Deterministischer Schlüssel aus Query-Fingerprint.
     * @param callable $resolveCallback Liefert das Ergebnis bei einem Cache-Miss.
     * @param array $tags Cache-Tags für gezielte Invalidierung.
     * @return array
     */
    public function resolveWithCache(string $cacheKey, callable $resolveCallback, array $tags): array
    {
        $cached = $this->cache->load($cacheKey);
        if ($cached !== false) {
            return $this->serializer->unserialize($cached);
        }

        $result = $resolveCallback();
        $this->cache->save($this->serializer->serialize($result), $cacheKey, $tags, 3600);

        return $result;
    }
}

4. Cache-Key-Generierung bei variablen Query-Parametern

Der kritische Punkt jeder GraphQL-Caching-Strategie ist die Ableitung eines deterministischen Cache-Schlüssels aus Query, Variablen und Kontext. Zwei syntaktisch unterschiedliche, aber semantisch identische Queries, etwa mit vertauschter Feldreihenfolge oder zusätzlichen Whitespaces, müssen auf denselben Schlüssel abgebildet werden, sonst entstehen unnötig viele Cache-Einträge für eigentlich dieselbe Anfrage. Eine bewährte Lösung normalisiert die Query zunächst, etwa durch Parsen und erneutes, kanonisches Serialisieren des Abstract Syntax Tree, bevor daraus ein Hash gebildet wird.

Zusätzlich zum Query-Fingerprint muss der Schlüssel Store-View, Währung und, falls relevant, die Customer-Group-ID einbeziehen, da dieselbe Query je nach Kontext unterschiedliche Preise oder Sichtbarkeiten liefern kann. Kundenspezifische Variablen wie eine Customer-ID gehören dagegen bewusst nicht in den Standard-Schlüssel, sondern werden, wie im nächsten Abschnitt beschrieben, gesondert behandelt oder komplett vom Caching ausgenommen.


<?php
declare(strict_types=1);

/**
 * Erzeugt einen deterministischen Cache-Schlüssel aus Query, Variablen
 * und Kontextfaktoren wie Store und Währung.
 *
 * @param string $normalizedQuery Kanonisch serialisierte, normalisierte Query.
 * @param array $variables GraphQL-Variablen der Anfrage.
 * @param int $storeId Aktuelle Store-View-ID.
 * @param string $currencyCode Aktueller Währungscode.
 * @return string
 */
function buildGraphQlCacheKey(
    string $normalizedQuery,
    array $variables,
    int $storeId,
    string $currencyCode
): string {
    ksort($variables);
    $fingerprint = hash('sha256', $normalizedQuery . json_encode($variables));

    return sprintf('gql_%d_%s_%s', $storeId, $currencyCode, $fingerprint);
}

5. Invalidierungsstrategie bei GraphQL-spezifischen Datenänderungen

Weil ein einzelner Redis-Schlüssel oft mehrere zusammengesetzte Resolver-Ergebnisse bündelt, reicht eine reine TTL-basierte Ablaufsteuerung selten aus. Stattdessen sollte jeder Cache-Eintrag mit denselben granularen Tags versehen werden, die Magento auch für klassisches FPC verwendet, etwa cat_p_123 für ein bestimmtes Produkt oder cat_c_45 für eine Kategorie, ergänzt um GraphQL-spezifische Tags für zusammengesetzte Resolver, die mehrere Entitäten gleichzeitig abfragen, etwa eine Produktlisten-Query mit Filterfacetten.

Beim Speichern eines Produkts oder einer Kategorie löst Magentos bestehende Indexer- und Cache-Invalidierungslogik ohnehin bereits die passenden Tags aus. Ein zusätzlicher Observer auf catalog_product_save_after oder vergleichbare Events kann diese Tags gezielt an die eigene GraphQL-Cache-Schicht weiterreichen, sodass nur betroffene Redis-Schlüssel gelöscht werden, während unveränderte Query-Ergebnisse für andere Produkte weiterhin aus dem Cache bedient werden.

6. Umgang mit personalisierten Feldern in zusammengesetzten Queries

Sobald eine Query personenbezogene Felder enthält, etwa den Warenkorb, hinterlegte Adressen oder individuelle Preisregeln eines eingeloggten Kunden, darf das betroffene Teilergebnis grundsätzlich nicht unter einem globalen, kundenunabhängigen Schlüssel landen. Eine robuste Strategie trennt Resolver in zwei Kategorien: öffentliche, store-weite Resolver, deren Ergebnis für alle Besucher identisch ist und aggressiv gecacht werden darf, und personalisierte Resolver, die entweder komplett vom Caching ausgenommen oder unter einem kundenspezifischen, kurzlebigen Schlüssel abgelegt werden.

In der Praxis bewährt sich eine Allowlist statt einer Blocklist: Nur Resolver, die explizit als cacheable markiert wurden, landen überhaupt in Redis. Alles andere wird bei jedem Request frisch aufgelöst. Dieser konservative Ansatz verhindert zuverlässig, dass durch ein übersehenes Feld versehentlich personenbezogene Daten eines Kunden an einen anderen ausgeliefert werden.

7. TTL-Design und Vermeidung von Cache-Stampedes bei teuren Queries

Sehr teure, rechenintensive Queries, etwa komplexe Facettensuchen mit vielen Filtern, sollten eine längere TTL erhalten als einfache Punktabfragen, da ihre Neuberechnung deutlich mehr Datenbank- und CPU-Last erzeugt. Läuft ein solcher Schlüssel gleichzeitig für sehr viele parallele Requests ab, kann es zu einem Cache-Stampede kommen, bei dem plötzlich zahlreiche Anfragen gleichzeitig denselben teuren Resolver auslösen, weil der Cache-Eintrag exakt im selben Moment abgelaufen ist.

Ein einfacher, wirksamer Schutz ist ein probabilistisches Early-Expiration-Verfahren, bei dem ein kleiner, zufälliger Vorlauf vor dem tatsächlichen Ablauf eine einzelne Neuberechnung auslöst, während alle anderen parallelen Requests noch den alten, aber noch gültigen Wert erhalten. Alternativ lässt sich mit SET key value NX PX ein kurzzeitiger Lock setzen, der verhindert, dass mehrere Prozesse gleichzeitig denselben teuren Resolver ausführen, während die übrigen Requests kurz auf das Ergebnis des ersten warten.

8. Monitoring: Hit-Rate und Schlüssel-Kardinalität im Blick behalten

Ohne Monitoring bleibt eine GraphQL-Cache-Schicht ein Blackbox-Experiment. Sinnvoll ist ein eigener Zähler pro Resolver-Typ, der Cache-Hits und Cache-Misses getrennt erfasst, sodass sich schnell erkennen lässt, welche Query-Muster tatsächlich von der Caching-Schicht profitieren und welche mangels Wiederverwendung praktisch immer einen Miss produzieren. Eine dauerhaft niedrige Hit-Rate bei einem bestimmten Resolver ist ein deutliches Signal, dass entweder der Cache-Key zu spezifisch generiert wird oder die betroffene Query in der Praxis kaum wiederholt aufgerufen wird.

Parallel dazu lohnt sich ein Blick auf die reine Schlüssel-Anzahl im GraphQL-Namensraum über redis-cli --scan --pattern 'gql_*' | wc -l, um eine unkontrolliert wachsende Kardinalität frühzeitig zu erkennen, etwa wenn durch unnormalisierte Variablen faktisch für jede Anfrage ein neuer, nie wiederverwendeter Schlüssel entsteht.


# Anzahl der GraphQL-Cache-Schlüssel im Namensraum zählen
redis-cli --scan --pattern 'gql_*' | wc -l

# Speicherverbrauch des GraphQL-Cache-Namensraums grob abschätzen
redis-cli --scan --pattern 'gql_*' | while read -r key; do
    redis-cli memory usage "$key"
done | awk '{sum+=$1} END {print sum, "bytes"}'

9. Grenzen und Fallstricke der eigenen GraphQL-Caching-Schicht

Eine selbst gebaute Query-Level-Caching-Schicht ist zusätzlicher Code, der gewartet, getestet und bei jedem Magento-Upgrade auf Kompatibilität mit dem Resolver-Schema geprüft werden muss. Ändert sich die interne Struktur eines Resolvers, etwa durch ein neues Pflichtfeld im Schema, muss auch die Cache-Key-Generierung und die Invalidierungslogik entsprechend angepasst werden, sonst drohen veraltete oder inkonsistente Antworten.

Außerdem lohnt sich der Aufwand vor allem bei wiederkehrenden, ähnlichen Queries mit hohem Traffic, etwa bei einem Storefront-Frontend mit begrenztem, gut vorhersehbarem Query-Repertoire. Bei sehr heterogenen, dynamisch generierten Queries, wie sie manche GraphQL-Client-Bibliotheken mit automatischer Query-Zusammensetzung erzeugen, bleibt die Hit-Rate oft gering, und der zusätzliche Redis-Zugriff kann die Latenz gegenüber einem direkten Resolver-Aufruf sogar leicht erhöhen.

Aspekt Klassisches FPC Redis Query-Level-Caching Praxisrelevanz
Cache-Einheit Komplette HTML-Seite pro URL Einzelnes Resolver-Teilergebnis Feinere Granularität bei GraphQL
Cache-Key URL plus X-Magento-Vary Query-Fingerprint plus Store/Währung Braucht eigene Normalisierung
Invalidierung Globale Tags pro Entität Gleiche Tags, aber pro Teilergebnis Weniger Überinvalidierung
Personalisierung Vary-Header pro Segment Explizite Allowlist cacheable Resolver Höheres Fehlerrisiko ohne Allowlist
Geeignet für Klassische Seitenaufrufe Headless-Frontends mit hohem GraphQL-Traffic Zusatzaufwand nur bei echtem Bedarf

Mironsoft

Cache-Layer-Setup und Magento-Redis-Integration

Magento-Cache, der nicht richtig greift oder falsch konfiguriert ist?

Wir richten Redis als Cache- und Session-Backend für Magento sauber ein, tunen Speicherverbrauch und Eviction-Strategien und sorgen dafür, dass Full Page Cache und Session-Storage zuverlässig zusammenspielen.

Redis-Setup

Cache-, Session- und FPC-Backend produktionsreif für Magento konfigurieren.

Memory-Tuning

Speicherverbrauch und Eviction-Policies auf die tatsächliche Shop-Last abstimmen.

High-Availability-Setup

Redis Sentinel oder Cluster für ausfallsichere Magento-Umgebungen einrichten.

10. Zusammenfassung

Redis-GraphQL-Caching in Magento: Das Wichtigste auf einen Blick

Ausgangsproblem

Ein einzelner GraphQL-Endpunkt mit variablen Query-Strings lässt sich nicht über URL-basiertes Full Page Caching abdecken.

Lösungsansatz

Einzelne Resolver-Teilergebnisse werden granular in Redis gecacht, mit deterministischen Schlüsseln aus normalisierter Query, Variablen und Store-Kontext.

Invalidierung

Dieselben Tags wie beim klassischen FPC, ergänzt um GraphQL-spezifische Tags für zusammengesetzte Resolver, sorgen für gezieltes Löschen statt globalem Flush.

Grenzen

Lohnt sich vor allem bei wiederkehrenden Queries mit hohem Traffic, erzeugt aber zusätzlichen Wartungsaufwand und Fehlerrisiko bei personalisierten Feldern.

11. FAQ: Redis-GraphQL-Caching in Magento: Das Wichtigste auf einen Blick

1Warum reicht klassisches Full Page Caching für Magento GraphQL nicht aus?
Alle Anfragen laufen über denselben Endpunkt, und erst der Query-String im Request-Body bestimmt die Antwort, wodurch eine URL-basierte Cache-Strategie nicht mehr greift.
2Cacht Magento GraphQL-Antworten nicht bereits über das eingebaute FPC?
Für als cacheable markierte, nicht personalisierte Resolver setzt Magento bereits Cache-Control- und X-Magento-Tags-Header, die von Varnish oder dem Redis-gestützten FPC genutzt werden können.
3Wann lohnt sich eine eigene Query-Level-Caching-Schicht trotzdem?
Vor allem bei headless Setups ohne vorgeschaltetes FPC oder bei zusammengesetzten Queries mit gemischter Cachebarkeit einzelner Resolver innerhalb einer Anfrage.
4Wie wird ein deterministischer Cache-Schlüssel aus einer GraphQL-Query erzeugt?
Die Query wird zunächst normalisiert und kanonisch serialisiert, anschließend mit sortierten Variablen sowie Store-ID und Währung zu einem Hash kombiniert.
5Wie werden personalisierte Felder beim Caching behandelt?
Über eine Allowlist werden nur explizit als öffentlich markierte Resolver gecacht, personalisierte Resolver mit kundenspezifischen Daten bleiben grundsätzlich außen vor.
6Welche Tags werden für die Invalidierung genutzt?
Dieselben granularen Tags wie beim klassischen FPC, etwa produkt- oder kategoriebezogene IDs, ergänzt um zusätzliche Tags für zusammengesetzte Resolver-Ergebnisse.
7Was ist ein Cache-Stampede im GraphQL-Kontext?
Wenn ein stark genutzter Cache-Eintrag abläuft und gleichzeitig sehr viele parallele Requests denselben teuren Resolver erneut auslösen, statt auf ein bereits laufendes Ergebnis zu warten.
8Wie lässt sich ein Cache-Stampede vermeiden?
Durch probabilistische Early Expiration oder einen kurzzeitigen Redis-Lock per SET NX PX, der nur einem Prozess die Neuberechnung erlaubt, während andere kurz warten.
9Wie überwacht man die Effektivität der GraphQL-Cache-Schicht?
Über getrennte Hit- und Miss-Zähler pro Resolver-Typ sowie eine regelmäßige Prüfung der Schlüssel-Kardinalität im GraphQL-Namensraum per redis-cli SCAN.
10Für welche Magento-Setups lohnt sich der Aufwand am ehesten?
Für Headless-Frontends mit hohem, wiederkehrendem GraphQL-Traffic und einem begrenzten, vorhersehbaren Query-Repertoire, weniger bei sehr heterogenen, dynamisch generierten Queries.