Doctrine Second Level Cache produktiv einsetzen
AI generated
SF
{ }
Symfony · Doctrine ORM · Caching · Performance
Doctrine Second Level Cache produktiv einsetzen
Query-Performance ohne Redis-Umwege

Der Doctrine Second Level Cache reduziert wiederkehrende Datenbankzugriffe, indem er hydratisierte Entities über Requests hinweg im Cache hält. Richtig konfiguriert mit Cache-Regions, Concurrency-Strategien und einem Redis-Adapter sinkt die Datenbanklast spürbar, ohne dass Konsistenzprobleme entstehen.

18 Min. Lesezeit Second Level Cache · Cache-Regions · Redis · PSR-6 Symfony 7 · Doctrine ORM 3

1. Was der Second Level Cache wirklich löst

Der Doctrine Second Level Cache setzt an einer anderen Stelle an als der First Level Cache, den viele Entwickler bereits kennen. Der First Level Cache, auch Identity Map genannt, existiert nur innerhalb eines EntityManagers und damit nur innerhalb eines einzigen Requests. Sobald der Request beendet ist, ist auch der First Level Cache weg. Der Doctrine Second Level Cache hingegen speichert hydratisierte Entity-Daten in einem persistenten Cache-Backend wie Redis oder APCu, und zwar über Requests, teilweise sogar über Server-Instanzen hinweg. Das macht ihn zum entscheidenden Baustein, wenn dieselben Entities immer wieder gelesen werden, aber selten geschrieben werden.

In der Praxis trifft das auf viele Domänen zu: Produktkategorien in einem Shop, Länder- und Steuertabellen, Konfigurationswerte, Rollen und Berechtigungen. Diese Daten ändern sich selten, werden aber bei fast jedem Request gelesen. Ohne Doctrine Second Level Cache führt jeder dieser Lesezugriffe zu einer echten Datenbankabfrage, selbst wenn sich der Datensatz seit Stunden nicht verändert hat. Mit aktiviertem Second Level Cache liefert Doctrine die Entity direkt aus dem Cache-Backend, ohne die Datenbank überhaupt zu kontaktieren. Der Unterschied bei stark frequentierten Endpunkten ist nicht kosmetisch, sondern reduziert die Datenbanklast oft um einen zweistelligen Prozentsatz.

Wichtig ist die Abgrenzung: Der Doctrine Second Level Cache cached Entity-Daten, nicht Query-Ergebnisse im Sinne von SQL-Result-Sets. Das bedeutet, unterschiedliche Queries, die dieselbe Entity referenzieren, profitieren vom selben Cache-Eintrag. Diese Eigenschaft unterscheidet ihn grundlegend vom Query Cache, den wir später im Artikel gegenüberstellen. Wer den Second Level Cache richtig einsetzt, muss verstehen, dass er ein Entity-Cache ist, kein Ergebnis-Cache.

2. Aktivierung in Symfony und Doctrine

Die Aktivierung des Doctrine Second Level Cache in Symfony erfolgt zentral über die Doctrine-Bundle-Konfiguration. Standardmäßig ist die Funktion deaktiviert, weil sie Konsequenzen für die Datenkonsistenz hat, die nicht jedes Projekt eingehen möchte. Die Aktivierung erfolgt über den Schlüssel orm.second_level_cache.enabled, kombiniert mit einer Cache-Pool-Definition unter Symfonys Cache-Komponente. Diese Trennung ist bewusst: Doctrine kümmert sich um die Cache-Logik, Symfonys Cache-Komponente um das Backend.

Nach der grundlegenden Aktivierung braucht der Doctrine Second Level Cache mindestens eine Region-Konfiguration und einen Cache-Adapter. Ohne Cache-Adapter fällt Doctrine auf einen Array-Cache zurück, der nur innerhalb eines einzigen PHP-Prozesses lebt und in FPM-Umgebungen mit mehreren Workern praktisch nutzlos ist. Für produktive Deployments ist ein geteiltes Backend wie Redis Pflicht, damit alle PHP-FPM-Worker denselben Cache-Zustand sehen.


# config/packages/doctrine.yaml
doctrine:
    orm:
        auto_generate_proxy_classes: true
        second_level_cache:
            enabled: true
            region_cache_driver:
                type: pool
                pool: doctrine.system_cache_pool
            log_enabled: '%kernel.debug%'
            regions:
                default:
                    lifetime: 3600
                    cache_driver:
                        type: pool
                        pool: cache.app
                category_region:
                    lifetime: 7200
                    cache_driver:
                        type: pool
                        pool: cache.app

framework:
    cache:
        pools:
            cache.app:
                adapter: cache.adapter.redis
                provider: 'redis://redis:6379'

Ein häufiger Anfängerfehler beim Doctrine Second Level Cache: die Region-Konfiguration wird gesetzt, aber keine einzige Entity wird tatsächlich für das Caching markiert. Ohne das #[Cache]-Attribut auf der Entity-Klasse bleibt der Second Level Cache technisch aktiviert, aber praktisch wirkungslos, weil Doctrine für keine Entity Cache-Einträge anlegt. Der nächste Abschnitt zeigt, wie diese Markierung konkret aussieht.

3. Cache-Regions und Concurrency-Strategien

Eine Cache-Region im Doctrine Second Level Cache ist ein logisch getrennter Bereich innerhalb des Cache-Backends mit eigener Lebensdauer und eigener Concurrency-Strategie. Doctrine unterstützt drei Concurrency-Strategien, die festlegen, wie mit gleichzeitigen Lese- und Schreibzugriffen umgegangen wird. READ_ONLY ist die einfachste und schnellste Strategie: Einmal geschriebene Cache-Einträge werden nie aktualisiert, sondern nur invalidiert. Sie eignet sich für Daten, die praktisch statisch sind, etwa Länder- oder Währungslisten.

NONSTRICT_READ_WRITE erlaubt Updates, garantiert aber keine strikte Konsistenz zwischen Cache und Datenbank während gleichzeitiger Schreibvorgänge. Für die meisten Anwendungsfälle mit moderater Schreibfrequenz ist das ein guter Kompromiss zwischen Performance und Konsistenz. READ_WRITE ist die strengste Strategie im Doctrine Second Level Cache: Sie nutzt Soft-Locks, um während eines Updates konkurrierende Leseoperationen zu blockieren, und garantiert damit, dass niemals veraltete Daten nach einem Commit gelesen werden. Diese Strategie kostet mehr Overhead, ist aber bei Entities mit hoher Schreibfrequenz und strengen Konsistenzanforderungen notwendig.

Die Wahl der richtigen Concurrency-Strategie im Doctrine Second Level Cache ist keine rein technische Entscheidung, sondern eine fachliche. Ein Preisfeld in einem Produktkatalog, das mehrmals täglich durch Import-Jobs aktualisiert wird, braucht eine andere Strategie als eine Kategorienbezeichnung, die einmal im Jahr geändert wird. Wer diese Unterscheidung ignoriert und pauschal READ_WRITE für alles verwendet, verschenkt Performance ohne fachlichen Nutzen.

4. Entities mit dem Cache-Attribut markieren

Damit der Doctrine Second Level Cache für eine bestimmte Entity aktiv wird, muss die Klasse explizit mit dem #[Cache]-Attribut versehen werden. Das Attribut kann auf Klassenebene für die gesamte Entity gesetzt werden, aber auch auf einzelnen Assoziationen, etwa einer ManyToOne- oder OneToMany-Beziehung. Diese Granularität ist wichtig, weil nicht jede Assoziation dieselbe Cache-Strategie verdient. Eine Bestellung sollte nicht gecacht werden, ihre zugehörige Kategorie-Referenz hingegen schon.


<?php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Cache(usage: 'READ_ONLY', region: 'category_region')]
class Category
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private int $id;

    #[ORM\Column(length: 100)]
    private string $name;

    #[ORM\OneToMany(mappedBy: 'category', targetEntity: Product::class)]
    #[ORM\Cache(usage: 'NONSTRICT_READ_WRITE', region: 'category_region')]
    private Collection $products;

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

Bei Assoziationen greift eine wichtige Regel: Die zugehörige Ziel-Entity muss ebenfalls cachebar sein, damit die Assoziation selbst gecacht werden kann. Doctrine wirft eine Ausnahme, wenn eine Collection als cachebar markiert wird, deren Elemente selbst nicht mit #[Cache] versehen sind. Diese Konsistenzprüfung verhindert, dass der Doctrine Second Level Cache halbe, widersprüchliche Cache-Zustände produziert.

5. Redis als Second-Level-Cache-Adapter

Für produktive Symfony-Anwendungen ist Redis der naheliegendste Adapter für den Doctrine Second Level Cache, weil er PSR-6-kompatibel ist, hohe Durchsätze erlaubt und in nahezu jeder Deployment-Umgebung bereits vorhanden ist. Symfonys Cache-Komponente stellt mit cache.adapter.redis einen fertigen Adapter bereit, der lediglich eine Verbindungs-DSN benötigt. Wichtig ist, für den Second Level Cache einen eigenen Redis-Namespace oder eine eigene Datenbanknummer zu verwenden, damit Cache-Einträge nicht mit Session-Daten oder anderen Cache-Pools kollidieren.

Ein häufig übersehener Aspekt: Der Redis-Adapter im Doctrine Second Level Cache serialisiert Entity-Daten, nicht die Entity-Objekte selbst. Doctrine speichert einen internen Snapshot der Feldwerte und rekonstruiert daraus beim Cache-Hit ein neues Entity-Objekt über die reguläre Hydration-Pipeline. Das bedeutet, Serialisierungskosten entstehen bei jedem Cache-Zugriff, nicht nur beim Schreiben. Für sehr breite Entities mit vielen Feldern kann dieser Overhead spürbar werden, weshalb der Second Level Cache am meisten bei kompakten, häufig gelesenen Entities Sinn ergibt.

6. Query Cache versus Second Level Cache

Der Query Cache und der Doctrine Second Level Cache werden oft verwechselt, lösen aber unterschiedliche Probleme. Der Query Cache speichert die geparste, in SQL übersetzte DQL-Query, nicht die Ergebnisdaten. Er spart also die Kosten des DQL-Parsers und Query-Compilers bei wiederholt ausgeführten Queries mit identischer Struktur, aber unterschiedlichen Parametern. Die Datenbankabfrage selbst wird beim Query Cache trotzdem bei jedem Aufruf ausgeführt.

Der Second Level Cache hingegen kann die Datenbankabfrage komplett vermeiden, wenn alle referenzierten Entities bereits im Cache liegen. Für maximale Wirkung kombiniert man beide: Der Query Cache reduziert den Parsing-Overhead, der Second Level Cache reduziert die Anzahl der tatsächlichen Datenbankzugriffe. Eine Query kann zusätzlich explizit für den Second Level Cache markiert werden, mit $query->setCacheable(true), wodurch auch das reine Query-Result-Set als Region gecacht wird, unabhängig von den einzelnen Entity-Caches.

7. Invalidierung und Race Conditions vermeiden

Invalidierung ist die schwierigste Disziplin beim Doctrine Second Level Cache. Bei READ_WRITE-Regionen nutzt Doctrine eine Timestamp-Region, die für jede Region den Zeitpunkt der letzten Änderung festhält. Beim Lesen vergleicht Doctrine den Cache-Eintrag-Zeitstempel mit dem Regions-Zeitstempel und verwirft veraltete Einträge automatisch. Diese Timestamp-Region muss selbst ebenfalls in einem persistenten Cache-Backend liegen, sonst funktioniert die Invalidierung nach einem Prozess-Neustart nicht zuverlässig.

Eine Race Condition entsteht typischerweise, wenn zwei Prozesse gleichzeitig dieselbe Entity lesen und schreiben, während der Second Level Cache mit NONSTRICT_READ_WRITE konfiguriert ist. In diesem Fall kann kurzzeitig ein veralteter Wert aus dem Cache gelesen werden, bevor die Invalidierung greift. Für Daten, bei denen das inakzeptabel ist, etwa Kontostände oder Lagerbestände, ist READ_WRITE mit Soft-Locking die richtige Wahl, auch wenn es mehr Cache-Overhead erzeugt. Manuelle Invalidierung ist über $cache->evictEntity() und $cache->evictEntityRegion() möglich, etwa nach Bulk-Updates, die am ORM vorbei direkt per SQL geschrieben wurden.

8. Monitoring und Cache-Hit-Rate messen

Ohne Messung bleibt jede Aussage über den Nutzen des Doctrine Second Level Cache Spekulation. Doctrine bietet mit CacheLogger eine Schnittstelle, um Hits, Misses und Puts pro Region zu zählen. In Symfony lässt sich dieser Logger als Service registrieren und die Zähler regelmäßig an ein Monitoring-System wie Prometheus exportieren. Eine niedrige Hit-Rate bei einer als READ_ONLY markierten Region deutet fast immer auf ein Konfigurationsproblem hin, etwa eine zu kurze Cache-Lifetime oder einen Redis-Adapter, der Einträge vorzeitig verdrängt.

Der Symfony Profiler zeigt im Doctrine-Panel bei aktiviertem log_enabled zusätzlich, welche Queries durch den Second Level Cache vermieden wurden. Diese Ansicht ist während der Entwicklung Gold wert, um zu verifizieren, dass die Cache-Attribute tatsächlich greifen, bevor man sich auf die Performance-Verbesserung in Produktion verlässt. Ein sinnvoller Rollout-Prozess aktiviert den Second Level Cache zunächst für einzelne, unkritische Entities, misst die Hit-Rate über mehrere Tage und erweitert die Abdeckung schrittweise.

9. Second Level Cache im direkten Vergleich

Die folgende Tabelle stellt die unterschiedlichen Caching-Ebenen in Doctrine einander gegenüber und zeigt, wann welcher Ansatz sinnvoll ist. Die Wahl der richtigen Ebene entscheidet darüber, ob der Doctrine Second Level Cache tatsächlich Last von der Datenbank nimmt oder nur zusätzliche Komplexität ohne messbaren Effekt erzeugt.

Cache-Ebene Was wird gecacht Lebensdauer Typischer Einsatz
First Level Cache Identity Map im EntityManager Ein Request Automatisch, keine Konfiguration nötig
Query Cache Geparste DQL zu SQL Bis Deploy oder Cache-Clear Viele Query-Ausführungen, gleiche Struktur
Doctrine Second Level Cache Hydratisierte Entity-Daten Region-Lifetime, z.B. 3600s Häufig gelesene, selten geschriebene Entities
Result Cache Rohes Query-Ergebnis-Set Manuell definiert Einzelne, teure Reports oder Dashboards
Anwendungs-Cache (manuell) Beliebige berechnete Werte Frei wählbar Aggregationen, externe API-Antworten

Aus dieser Gegenüberstellung folgt eine klare Empfehlung: Der Doctrine Second Level Cache ist die richtige Wahl für Entities mit einem klaren Lese-Schreib-Ungleichgewicht. Für einmalige, teure Aggregationsabfragen ist ein manueller Anwendungs-Cache oft die pragmatischere Lösung, weil er nicht an die Entity-Struktur gebunden ist und flexibler invalidiert werden kann.

Mironsoft

Symfony-Performance, Doctrine-Optimierung und Caching-Architektur

Doctrine Cache-Strategie für euer Projekt aufsetzen?

Wir analysieren eure Entity-Zugriffsmuster, konfigurieren Cache-Regions mit den passenden Concurrency-Strategien und richten einen produktionstauglichen Redis-Adapter für den Doctrine Second Level Cache ein.

Cache-Audit

Analyse der Lese-Schreib-Muster und Empfehlung passender Regions

Redis-Setup

Produktionsreife Konfiguration mit Monitoring und Alerting

Performance-Tuning

Query-Analyse, Hit-Rate-Optimierung und Regressionstests

10. Zusammenfassung

Der Doctrine Second Level Cache löst ein konkretes Problem: wiederkehrende Datenbankzugriffe auf Entities, die sich selten ändern. Die Aktivierung erfordert drei Schritte, die zusammenspielen müssen: die Konfiguration in doctrine.yaml, einen persistenten Cache-Adapter wie Redis und das explizite Markieren von Entities mit dem #[Cache]-Attribut. Ohne den letzten Schritt bleibt der Second Level Cache wirkungslos, selbst wenn alle anderen Teile korrekt konfiguriert sind.

Die Wahl der Concurrency-Strategie, READ_ONLY, NONSTRICT_READ_WRITE oder READ_WRITE, sollte fachlich begründet sein und nicht pauschal für alle Entities gleich ausfallen. Monitoring über den CacheLogger und den Symfony Profiler zeigt, ob der Doctrine Second Level Cache tatsächlich greift, bevor man sich in Produktion auf die Performance-Verbesserung verlässt. Wer diese Schritte sauber umsetzt, reduziert Datenbanklast messbar, ohne die Konsistenz der Anwendung zu gefährden.

Doctrine Second Level Cache — Das Wichtigste auf einen Blick

Aktivierung

orm.second_level_cache.enabled: true plus Cache-Pool und mindestens eine Region-Definition in doctrine.yaml.

Cache-Attribut

#[ORM\Cache] auf Entity- und Assoziationsebene setzen, sonst bleibt der Cache trotz Aktivierung wirkungslos.

Concurrency-Strategie

READ_ONLY für statische Daten, NONSTRICT_READ_WRITE als Kompromiss, READ_WRITE für strenge Konsistenz.

Monitoring

CacheLogger und Symfony Profiler nutzen, um Hit-Rate zu messen und Fehlkonfigurationen früh zu erkennen.

11. FAQ: Doctrine Second Level Cache

1First Level Cache vs. Second Level Cache?
First Level lebt nur im EntityManager eines Requests. Second Level speichert persistent in Redis oder APCu, ueber Requests hinweg.
2Reicht doctrine.yaml Konfiguration allein?
Nein, jede Entity braucht zusaetzlich das Attribut #[ORM\Cache], sonst bleibt der Cache wirkungslos.
3Welche Concurrency-Strategie waehlen?
READ_ONLY fuer statische Daten, NONSTRICT_READ_WRITE als Kompromiss, READ_WRITE bei strengen Konsistenzanforderungen.
4Funktioniert es ohne Redis?
Technisch mit Array-Cache oder APCu, aber in Multi-Worker-Umgebungen ist Redis fuer geteilten Cache-Zustand notwendig.
5Unterschied zum Query Cache?
Query Cache speichert geparste SQL-Struktur, nicht Ergebnisse. Second Level Cache speichert Entity-Daten und vermeidet die Abfrage ganz.
6Manuelle Invalidierung nach SQL-Update?
Mit $cache->evictEntity() oder $cache->evictEntityRegion() fuer eine ganze Region.
7Wie Hit-Rate messen?
Mit dem CacheLogger von Doctrine und dem Symfony Profiler bei aktiviertem log_enabled.
8Koennen Assoziationen gecacht werden?
Ja, ueber #[ORM\Cache] auf der Assoziation, vorausgesetzt die Ziel-Entity ist ebenfalls cachebar markiert.
9Gibt es Serialisierungs-Overhead?
Ja, Doctrine serialisiert Feldwerte bei jedem Cache-Zugriff. Bei sehr breiten Entities kann das spuerbar werden.
10Wie starte ich den Rollout sicher?
Erst einzelne, unkritische Entities markieren, Hit-Rate ueber Tage beobachten, dann schrittweise erweitern.