Nette Caching

Der Cache beschleunigt Ihre Anwendung, indem er einmal aufwendig beschaffte Daten speichert und sie beim nächsten Mal schneller zur Verfügung stellt. Wir zeigen Ihnen:

  • wie Sie den Cache verwenden
  • wie Sie das Storage wechseln
  • wie Sie den Cache richtig invalidieren

Die Verwendung des Caches ist in Nette sehr einfach und deckt zugleich anspruchsvolle Anforderungen ab. Er ist auf Leistung und 100%ige Zuverlässigkeit ausgelegt. Für die gängigsten Storage-Backends sind bereits Adapter enthalten. Er unterstützt Invalidierung anhand von Tags, zeitlichen Ablauf, Schutz vor Cache Stampede und mehr.

Installation

Laden Sie das Paket mit Composer herunter und installieren Sie es:

composer require nette/caching

Grundlegende Verwendung

Der Mittelpunkt der Arbeit mit dem Cache ist das Objekt Nette\Caching\Cache. Wir erzeugen eine Instanz davon und übergeben dem Konstruktor ein sogenanntes Storage. Das ist ein Objekt, das den Ort repräsentiert, an dem die Daten physisch abgelegt werden (Datenbank, Memcached, Dateien auf der Festplatte, …). Das Storage lassen Sie sich üblicherweise per Dependency Injection mit dem Typ Nette\Caching\Storage übergeben. Alles Wesentliche erfahren Sie im Abschnitt Storages.

In Version 3.0 hatte das Interface noch das Präfix I, der Name lautete also Nette\Caching\IStorage. Außerdem wurden die Konstanten der Klasse Cache in Großbuchstaben geschrieben, also etwa Cache::EXPIRE statt Cache::Expire.

Für die folgenden Beispiele nehmen wir an, dass wir einen Alias Cache und in der Variable $storage ein Storage haben.

use Nette\Caching\Cache;

$storage = /* ... */; // Instanz von Nette\Caching\Storage

Der Cache ist im Grunde ein Key-Value-Store, wir lesen und schreiben die Daten also unter Schlüsseln, ganz ähnlich wie bei assoziativen Arrays. Anwendungen bestehen aus einer Reihe unabhängiger Teile, und würden alle ein einziges Storage nutzen (stellen Sie sich ein einziges Verzeichnis auf der Festplatte vor), käme es früher oder später zu Kollisionen der Schlüssel. Das Nette Framework löst das Problem, indem es den gesamten Raum in Namensräume (also gedanklich Unterverzeichnisse) aufteilt. Jeder Teil des Programms arbeitet dann in seinem eigenen Raum mit einem eindeutigen Namen, und zu einer Kollision kann es nicht mehr kommen.

Den Namen des Raums geben wir als zweiten Parameter des Konstruktors der Klasse Cache an:

$cache = new Cache($storage, 'Full Html Pages');

Bei Bedarf lässt sich aus einer bestehenden Instanz mit der Methode derive() ein neuer Cache in einem verschachtelten Unterraum ableiten:

$subCache = $cache->derive('Images');

Jetzt können wir mit dem Objekt $cache aus dem Cache lesen und in ihn schreiben. Für beides dient die Methode load(). Das erste Argument ist der Schlüssel, das zweite ein PHP-Callback, das aufgerufen wird, wenn der Schlüssel im Cache nicht gefunden wird. Das Callback erzeugt den Wert, gibt ihn zurück, und die Methode load() legt ihn im Cache ab:

$value = $cache->load($key, function () use ($key) {
	$computedValue = /* ... */; // aufwendige Berechnung
	return $computedValue;
});

Wenn wir den zweiten Parameter weglassen ($value = $cache->load($key)), gibt load() null zurück, falls der Eintrag nicht im Cache liegt.

Praktisch ist, dass sich beliebige serialisierbare Strukturen im Cache ablegen lassen, nicht nur Strings. Und dasselbe gilt sogar für die Schlüssel.

Einen Eintrag löschen wir mit der Methode remove() aus dem Cache:

$cache->remove($key);

Einen Eintrag können Sie auch mit der Methode $cache->save($key, $data, ?array $dependencies = null) im Cache speichern. Bevorzugt wird jedoch der oben gezeigte Weg über load().

Memoization

Memoization bedeutet, das Ergebnis eines Funktions- oder Methodenaufrufs zu cachen, damit Sie es beim nächsten Mal verwenden können, ohne dasselbe immer wieder neu zu berechnen.

Methoden und Funktionen lassen sich mit call(callable $callback, ...$args) memoisiert aufrufen:

$result = $cache->call('gethostbyaddr', $ip);

Die Funktion gethostbyaddr() wird so für jeden Parameter $ip nur einmal aufgerufen, beim nächsten Mal mit demselben $ip kommt der Wert bereits aus dem Cache.

Es ist auch möglich, einen memoisierten Wrapper um eine Methode oder Funktion zu erzeugen, der sich erst später aufrufen lässt:

function factorial($num)
{
	return /* ... */;
}

$memoizedFactorial = $cache->wrap('factorial');

$result = $memoizedFactorial(5); // berechnet es beim ersten Mal
$result = $memoizedFactorial(5); // beim zweiten Mal aus dem Cache

Ablauf & Invalidierung

Beim Ablegen im Cache muss die Frage geklärt werden, wann zuvor gespeicherte Daten ungültig werden. Das Nette Framework bietet einen Mechanismus, mit dem sich die Gültigkeit der Daten begrenzen oder die Daten gezielt löschen lassen (in der Terminologie des Frameworks “invalidieren”).

Die Gültigkeit der Daten wird im Moment des Speicherns festgelegt, und zwar über den dritten Parameter der Methode save(), z. B.:

$cache->save($key, $value, [
	$cache::Expire => '20 minutes',
]);

Oder über den Parameter $dependencies, der dem Callback der Methode load() per Referenz übergeben wird, z. B.:

$value = $cache->load($key, function (&$dependencies) {
	$dependencies[Cache::Expire] = '20 minutes';
	return /* ... */;
});

Oder über den 3. Parameter der Methode load(), z. B.:

$value = $cache->load($key, function () {
	return /* ... */;
}, [Cache::Expire => '20 minutes']);

In den folgenden Beispielen gehen wir von der zweiten Variante aus, also von der Existenz der Variable $dependencies.

Ablauf

Die einfachste Form des Ablaufs ist ein Zeitlimit. So legen wir Daten mit einer Gültigkeit von 20 Minuten im Cache ab:

// akzeptiert auch eine Anzahl von Sekunden oder einen UNIX-Timestamp
$dependencies[Cache::Expire] = '20 minutes';

Wenn sich die Gültigkeitsdauer mit jedem Lesen verlängern soll, erreichen Sie das folgendermaßen; beachten Sie aber, dass der Overhead des Caches dadurch steigt:

$dependencies[Cache::Sliding] = true;

Praktisch ist die Möglichkeit, Daten in dem Moment ablaufen zu lassen, in dem sich eine Datei oder eine von mehreren Dateien ändert. Das lässt sich etwa nutzen, wenn Sie Daten im Cache ablegen, die aus der Verarbeitung dieser Dateien entstanden sind. Verwenden Sie absolute Pfade.

$dependencies[Cache::Files] = '/path/to/data.yaml';
// oder
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];

Wir können einen Eintrag im Cache in dem Moment ablaufen lassen, in dem ein anderer Eintrag (oder einer von mehreren anderen) abläuft. Das lässt sich nutzen, wenn wir etwa eine ganze HTML-Seite und unter anderen Schlüsseln ihre Fragmente im Cache ablegen. Sobald sich ein Fragment ändert, wird die ganze Seite invalidiert. Haben wir die Fragmente unter den Schlüsseln frag1 und frag2 gespeichert, verwenden wir:

$dependencies[Cache::Items] = ['frag1', 'frag2'];

Der Ablauf lässt sich auch über eigene Funktionen oder statische Methoden steuern, die bei jedem Lesen entscheiden, ob der Eintrag noch gültig ist. So können wir einen Eintrag etwa immer dann ablaufen lassen, wenn sich die PHP-Version ändert. Wir erstellen eine Funktion, die die aktuelle Version mit einem Parameter vergleicht, und fügen beim Speichern den Abhängigkeiten ein Array in der Form [Funktionsname, ...Argumente] hinzu:

function checkPhpVersion($ver): bool
{
	return $ver === PHP_VERSION_ID;
}

$dependencies[Cache::Callbacks] = [
	['checkPhpVersion', PHP_VERSION_ID] // ablaufen lassen, wenn checkPhpVersion(...) === false
];

Alle diese Kriterien lassen sich selbstverständlich kombinieren. Der Cache läuft dann ab, wenn mindestens ein Kriterium nicht mehr erfüllt ist.

$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';

Invalidierung mittels Tags

Ein sehr nützliches Werkzeug zur Invalidierung sind sogenannte Tags. Jedem Eintrag im Cache können wir eine Liste von Tags zuweisen, also beliebige Strings. Nehmen wir etwa eine HTML-Seite mit einem Artikel und Kommentaren, die wir cachen wollen. Beim Speichern geben wir die Tags an:

$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];

Wechseln wir nun in die Administration. Dort finden wir ein Formular zum Bearbeiten des Artikels. Zusammen mit dem Speichern des Artikels in der Datenbank rufen wir die Methode clean() auf, die Einträge anhand des Tags aus dem Cache löscht:

$cache->clean([
	$cache::Tags => ["article/$articleId"],
]);

Ebenso vergessen wir dort, wo ein neuer Kommentar hinzugefügt (oder ein Kommentar bearbeitet) wird, nicht, den entsprechenden Tag zu invalidieren:

$cache->clean([
	$cache::Tags => ["comments/$articleId"],
]);

Was haben wir damit erreicht? Dass unser HTML-Cache immer dann invalidiert (gelöscht) wird, wenn sich der Artikel oder die Kommentare ändern. Wird der Artikel mit der ID = 10 bearbeitet, wird der Tag article/10 zwangsweise invalidiert und die HTML-Seite, die diesen Tag trägt, aus dem Cache gelöscht. Dasselbe passiert beim Einfügen eines neuen Kommentars unter dem betreffenden Artikel.

Tags erfordern ein sogenanntes Journal.

Invalidierung mittels Priorität

Einzelnen Einträgen im Cache können wir eine Priorität zuweisen, mit deren Hilfe sie sich löschen lassen, wenn der Cache etwa eine bestimmte Größe überschreitet:

$dependencies[Cache::Priority] = 50;

So löschen wir alle Einträge mit einer Priorität gleich oder kleiner als 100:

$cache->clean([
	$cache::Priority => 100,
]);

Prioritäten erfordern ein sogenanntes Journal.

Löschen des Caches

Der Parameter Cache::All löscht alles:

$cache->clean([
	$cache::All => true,
]);

Massenlesen

Für das massenhafte Lesen und Schreiben in den Cache dient die Methode bulkLoad(), der wir ein Array von Schlüsseln übergeben und ein Array von Werten erhalten:

$values = $cache->bulkLoad($keys);

Die Methode bulkLoad() funktioniert ähnlich wie load(), auch mit einem Callback als zweitem Parameter, dem der Schlüssel des erzeugten Eintrags übergeben wird:

$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
	$computedValue = /* ... */; // aufwendige Berechnung
	return $computedValue;
});

Umgekehrt dient für das gleichzeitige Schreiben mehrerer Einträge die Methode bulkSave(), der wir ein Array von Paaren Schlüssel => Wert und optional Abhängigkeiten übergeben:

$cache->bulkSave([
	$key1 => $value1,
	$key2 => $value2,
], [Cache::Expire => '20 minutes']);

Verwendung mit PSR-16

Um Nette Cache mit dem Interface PSR-16 zu verwenden, können Sie den Adapter PsrCacheAdapter nutzen. Er ermöglicht eine reibungslose Integration zwischen Nette Cache und beliebigem Code oder beliebigen Bibliotheken, die eine PSR-16-kompatible Cache-Implementierung erwarten.

$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);

Jetzt können Sie $psrCache als PSR-16-Cache verwenden:

$psrCache->set('key', 'value', 3600); // speichert den Wert für 1 Stunde
$value = $psrCache->get('key', 'default');

Der Adapter unterstützt alle in PSR-16 definierten Methoden, einschließlich getMultiple(), setMultiple() und deleteMultiple().

Caching der Ausgabe

Sehr elegant lässt sich die Ausgabe abfangen und cachen:

if ($capture = $cache->capture($key)) {

	// echo ... wir geben Daten aus

	$capture->end(); // die Ausgabe im Cache speichern
}

Ist die Ausgabe bereits im Cache gespeichert, gibt die Methode capture() sie aus und liefert null zurück, der Block der if-Bedingung wird also übersprungen. Andernfalls beginnt sie, die Ausgabe abzufangen, und gibt ein Objekt $capture zurück, mit dem wir die ausgegebenen Daten am Ende über dessen Methode end() im Cache speichern.

In Version 3.0 hieß die Methode $cache->start().

Caching in Latte

Caching in Latte-Templates ist sehr einfach, es genügt, den zu cachenden Teil des Templates mit den Tags {cache}...{/cache} zu umschließen. Der Cache wird automatisch in dem Moment invalidiert, in dem sich das Quell-Template ändert (einschließlich eventuell eingebundener Templates innerhalb des Cache-Blocks). Die Tags {cache} lassen sich ineinander verschachteln, und wird ein verschachtelter Block ungültig (etwa über einen Tag), wird auch der übergeordnete Block ungültig.

Im Tag lassen sich die Schlüssel angeben, an die der Cache gebunden wird (hier die Variable $id), eine Ablaufzeit setzen und Tags für die Invalidierung festlegen.

{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
	...
{/cache}

Alle Angaben sind optional, wir müssen also weder die Ablaufzeit noch die Tags noch überhaupt die Schlüssel angeben.

Die Verwendung des Caches lässt sich auch mit if an eine Bedingung knüpfen – der Inhalt wird dann nur gecacht, wenn die Bedingung erfüllt ist:

{cache $id, if: !$form->isSubmitted()}
	{$form}
{/cache}

Storages

Ein Storage ist ein Objekt, das den Ort repräsentiert, an dem die Daten physisch abgelegt werden. Wir können eine Datenbank, einen Memcached-Server oder das am leichtesten verfügbare Storage nutzen: Dateien auf der Festplatte.

Storage Beschreibung
FileStorage Standard-Storage, speichert den Cache in Dateien auf der Festplatte.
MemcachedStorage Nutzt einen Memcached-Server als Ablageort.
MemoryStorage Daten liegen vorübergehend im Arbeitsspeicher (gehen am Ende des Requests verloren).
SQLiteStorage Daten werden in einer SQLite-Datenbankdatei abgelegt.
DevNullStorage Daten werden gar nicht gespeichert, nützlich zum Testen.

Das Storage lassen Sie sich per Dependency Injection mit dem Typ Nette\Caching\Storage übergeben. Als Standard-Storage stellt Nette ein FileStorage-Objekt bereit, das die Daten im Unterordner cache innerhalb des Verzeichnisses für temporäre Dateien ablegt.

Das Storage können Sie in der Konfiguration ändern:

services:
	cache.storage: Nette\Caching\Storages\DevNullStorage

FileStorage

Schreibt den Cache in Dateien auf der Festplatte. Das Storage Nette\Caching\Storages\FileStorage ist sehr gut auf Leistung optimiert und stellt vor allem die volle Atomizität der Operationen sicher. Was bedeutet das? Dass bei der Verwendung des Caches nicht passieren kann, dass wir eine Datei lesen, die ein anderer Thread noch nicht vollständig geschrieben hat, oder dass sie jemand “unter unseren Händen” löscht. Die Verwendung des Caches ist also völlig sicher.

Dieses Storage hat außerdem eine wichtige eingebaute Funktion, die einen extremen Anstieg der CPU-Auslastung in dem Moment verhindert, in dem der Cache gelöscht wird oder noch nicht aufgewärmt (also noch nicht erzeugt) ist. Es handelt sich um die Vorbeugung gegen Cache Stampede. Es kommt vor, dass zu einem Zeitpunkt eine größere Zahl gleichzeitiger Requests zusammentrifft, die dieselbe Sache aus dem Cache haben wollen (etwa das Ergebnis einer teuren SQL-Query), und weil sie nicht im Cache liegt, beginnen alle Prozesse dieselbe SQL-Query auszuführen. Die Auslastung vervielfacht sich dadurch, und es kann sogar passieren, dass kein Thread es schafft, im Zeitlimit zu antworten, der Cache nicht entsteht und die Anwendung zusammenbricht. Zum Glück funktioniert der Cache in Nette so, dass bei mehreren gleichzeitigen Requests auf einen Eintrag nur der erste Thread ihn erzeugt, die übrigen warten und nutzen anschließend das erzeugte Ergebnis.

Beispiel für das Erzeugen eines FileStorage:

// das Storage ist das Verzeichnis '/path/to/temp' auf der Festplatte
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

Der Server Memcached ist ein hochperformantes System zum Ablegen von Objekten im verteilten Arbeitsspeicher, dessen Adapter Nette\Caching\Storages\MemcachedStorage ist. In der Konfiguration geben wir die IP-Adresse und den Port an, falls er vom Standard 11211 abweicht.

Erfordert die PHP-Erweiterung memcached.

services:
	cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')

MemoryStorage

Nette\Caching\Storages\MemoryStorage ist ein Storage, das die Daten in einem PHP-Array ablegt und sie also mit dem Ende des Requests verliert.

SQLiteStorage

Die Datenbank SQLite und der Adapter Nette\Caching\Storages\SQLiteStorage bieten eine Möglichkeit, den Cache in einer einzigen Datei auf der Festplatte abzulegen. In der Konfiguration geben wir den Pfad zu dieser Datei an.

Erfordert die PHP-Erweiterungen pdo und pdo_sqlite.

services:
	cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')

DevNullStorage

Eine besondere Implementierung eines Storage ist Nette\Caching\Storages\DevNullStorage, das in Wirklichkeit überhaupt keine Daten speichert. Es eignet sich damit zum Testen, wenn wir den Einfluss des Caches ausschalten wollen.

Verwendung des Caches im Code

Bei der Verwendung des Caches im Code haben wir zwei Möglichkeiten. Die erste besteht darin, uns per Dependency Injection das Storage übergeben zu lassen und das Objekt Cache zu erzeugen:

use Nette;

class ClassOne
{
	private Nette\Caching\Cache $cache;

	public function __construct(Nette\Caching\Storage $storage)
	{
		$this->cache = new Nette\Caching\Cache($storage, 'my-namespace');
	}
}

Die zweite Möglichkeit ist, uns gleich das Objekt Cache übergeben zu lassen:

class ClassTwo
{
	public function __construct(
		private Nette\Caching\Cache $cache,
	) {
	}
}

Das Objekt Cache wird dann direkt in der Konfiguration auf diese Weise erzeugt:

services:
	- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )

Journal

Nette legt Tags und Prioritäten in einem sogenannten Journal ab. Standardmäßig wird dafür SQLite und die Datei journal.s3db verwendet, und die PHP-Erweiterungen pdo und pdo_sqlite sind erforderlich.

Das Journal können Sie in der Konfiguration ändern:

services:
	cache.journal: MyJournal

DI-Services

Diese Services werden dem DI-Container hinzugefügt:

Name Typ Beschreibung
cache.journal Nette\Caching\Storages\Journal Storage des Cache-Journals
cache.storage Nette\Caching\Storage Primäres Cache-Storage

Cache abschalten

Eine der Möglichkeiten, den Cache in der Anwendung abzuschalten, ist, als Storage DevNullStorage zu setzen:

services:
	cache.storage: Nette\Caching\Storages\DevNullStorage

Diese Einstellung hat keinen Einfluss auf das Caching der Templates in Latte oder des DI-Containers, denn diese Bibliotheken nutzen die Services von nette/caching nicht und verwalten ihren Cache eigenständig. Ihr Cache muss im Entwicklungsmodus ohnehin nicht abgeschaltet werden.

Wenn Sie auf eine neuere Version aktualisieren, sehen Sie sich die Seite Upgrade an.

Version: 3.x