Cache

Der Cache [kɛʃ] beschleunigt Ihre Anwendung, indem er einmal aufwendig abgerufene Daten für die zukünftige Verwendung speichert. Wir zeigen Ihnen:

  • wie man den Cache verwendet
  • wie man den Speicher (Storage) ändert
  • wie man den Cache korrekt invalidiert

Die Verwendung des Caches ist in Nette sehr einfach und deckt dennoch auch sehr fortgeschrittene Anforderungen ab. Er ist auf Leistung und 100%ige Stabilität ausgelegt. Standardmäßig finden Sie Adapter für die gängigsten Backend-Speicher. Er ermöglicht die auf Tags basierende Invalidierung, Zeitablauf, Schutz vor Cache Stampede usw.

Installation

Sie können die Bibliothek mit dem Werkzeug Composer herunterladen und installieren:

composer require nette/caching

Grundlegende Verwendung

Der zentrale Punkt der Arbeit mit dem Cache oder Zwischenspeicher ist das Objekt Nette\Caching\Cache. Wir erstellen eine Instanz davon und übergeben dem Konstruktor als Parameter einen sogenannten Speicher (Storage). Dies ist ein Objekt, das den Ort darstellt, an dem die Daten physisch gespeichert werden (Datenbank, Memcached, Dateien auf der Festplatte, …). Zum Speicher gelangen wir, indem wir ihn uns mittels Dependency Injection mit dem Typ Nette\Caching\Storage übergeben lassen. Alles Wesentliche erfahren Sie im Abschnitt Speicher.

In Version 3.0 hatte das Interface noch das Präfix I, der Name war also Nette\Caching\IStorage. Außerdem wurden die Konstanten der Klasse Cache großgeschrieben, also z.B. Cache::EXPIRE statt Cache::Expire.

Für die folgenden Beispiele nehmen wir an, dass wir einen Alias Cache erstellt haben und der Speicher in der Variablen $storage vorhanden ist.

use Nette\Caching\Cache;

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

Der Cache ist eigentlich ein Key-Value-Store, das heißt, wir lesen und schreiben Daten unter Schlüsseln, genau wie bei assoziativen Arrays. Anwendungen bestehen aus einer Reihe unabhängiger Teile, und wenn alle denselben Speicher verwenden (stellen Sie sich ein Verzeichnis auf der Festplatte vor), würde es früher oder später zu Schlüsselkollisionen kommen. Das Nette Framework löst dieses Problem, indem es den gesamten Speicherplatz in Namensräume (Unterverzeichnisse) aufteilt. Jeder Teil des Programms verwendet dann seinen eigenen Namensraum mit einem eindeutigen Namen, und es kann keine Kollision mehr auftreten.

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

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

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

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

Wenn wir den zweiten Parameter nicht angeben $value = $cache->load($key), wird null zurückgegeben, wenn das Element nicht im Cache vorhanden ist.

Das Tolle ist, dass Sie beliebige serialisierbare Strukturen im Cache speichern können, nicht nur Strings. Und dasselbe gilt sogar für die Schlüssel.

Ein Element aus dem Cache löschen wir mit der Methode remove():

$cache->remove($key);

Ein Element kann auch mit der Methode $cache->save($key, $value, array $dependencies = []) im Cache gespeichert werden. Die bevorzugte Methode ist jedoch die oben beschriebene Verwendung von load().

Memoization

Memoization bedeutet das Cachen des Ergebnisses eines Funktions- oder Methodenaufrufs, sodass Sie es beim nächsten Mal verwenden können, ohne dasselbe erneut berechnen zu müssen.

Methoden und Funktionen können memoisiert mit call(callable $callback, ...$args) aufgerufen werden:

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

Die Funktion gethostbyaddr() wird somit für jeden Parameter $ip nur einmal aufgerufen, und beim nächsten Mal wird der Wert aus dem Cache zurückgegeben.

Es ist auch möglich, einen memoisierten Wrapper für eine Methode oder Funktion zu erstellen, der später aufgerufen werden kann:

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

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

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

Ablauf & Invalidierung

Beim Speichern im Cache muss die Frage geklärt werden, wann die zuvor gespeicherten Daten ungültig werden. Das Nette Framework bietet einen Mechanismus, um die Gültigkeit von Daten zu begrenzen oder sie kontrolliert zu löschen (in der Terminologie des Frameworks „invalidieren“).

Die Gültigkeit der Daten wird zum Zeitpunkt des Speicherns festgelegt, und zwar mit dem dritten Parameter der Methode save(), z.B.:

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

Oder mithilfe des Parameters $dependencies, der per Referenz an den Callback der Methode load() übergeben wird, z.B.:

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

Oder mithilfe des 3. Parameters in der Methode load(), z.B:

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

In den weiteren Beispielen gehen wir von der zweiten Variante und somit der Existenz der Variablen $dependencies aus.

Ablauf (Expiration)

Der einfachste Ablauf ist ein Zeitlimit. So speichern wir Daten für 20 Minuten im Cache:

// akzeptiert auch die Anzahl der Sekunden oder einen UNIX-Zeitstempel
$dependencies[Cache::Expire] = '20 minutes';

Wenn wir die Gültigkeitsdauer bei jedem Lesevorgang verlängern möchten (Sliding Expiration), kann dies wie folgt erreicht werden, aber Vorsicht, der Overhead des Caches steigt dadurch:

$dependencies[Cache::Sliding] = true;

Eine praktische Möglichkeit besteht darin, die Daten verfallen zu lassen, wenn sich eine Datei oder eine von mehreren Dateien ändert. Dies kann beispielsweise beim Speichern von Daten im Cache genutzt werden, die durch die 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 ein Element im Cache verfallen lassen, wenn ein anderes Element (oder eines von mehreren anderen) verfällt. Dies kann nützlich sein, wenn wir beispielsweise eine ganze HTML-Seite im Cache speichern und ihre Fragmente unter anderen Schlüsseln ablegen. Sobald sich ein Fragment ändert, wird die gesamte Seite invalidiert. Wenn die Fragmente beispielsweise unter den Schlüsseln frag1 und frag2 gespeichert sind, verwenden wir:

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

Der Ablauf kann auch über benutzerdefinierte Funktionen oder statische Methoden gesteuert werden, die bei jedem Lesevorgang entscheiden, ob das Element noch gültig ist. Auf diese Weise können wir beispielsweise ein Element immer dann verfallen lassen, wenn sich die PHP-Version ändert. Wir erstellen eine Funktion, die die aktuelle Version mit dem Parameter vergleicht, und beim Speichern fügen wir unter den Abhängigkeiten ein Array im Format [Funktionsname, ...Argumente] hinzu:

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

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

Natürlich können alle Kriterien kombiniert werden. Der Cache verfällt dann, wenn mindestens ein Kriterium nicht erfüllt ist.

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

Invalidierung mittels Tags

Ein sehr nützliches Invalidierungswerkzeug sind die sogenannten Tags. Jedem Element im Cache können wir eine Liste von Tags zuweisen, bei denen es sich um beliebige Strings handelt. Nehmen wir zum Beispiel eine HTML-Seite mit einem Artikel und Kommentaren, die wir cachen möchten. Beim Speichern geben wir die Tags an:

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

Wechseln wir zur Administration. Hier finden wir ein Formular zur Bearbeitung des Artikels. Zusammen mit dem Speichern des Artikels in der Datenbank rufen wir den Befehl clean() auf, der die Cache-Elemente entsprechend dem Tag löscht:

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

Ebenso vergessen wir an der Stelle, an der 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. Wenn der Artikel mit der ID = 10 bearbeitet wird, wird der Tag article/10 zwangsweise invalidiert, und die HTML-Seite, die diesen Tag trägt, wird aus dem Cache gelöscht. Dasselbe geschieht beim Einfügen eines neuen Kommentars unter dem entsprechenden Artikel.

Tags erfordern das sogenannte journal.

Invalidierung mittels Priorität

Einzelnen Elementen im Cache können wir eine Priorität zuweisen, mit der sie gelöscht werden können, wenn der Cache beispielsweise eine bestimmte Größe überschreitet:

$dependencies[Cache::Priority] = 50;

Wir löschen alle Elemente mit einer Priorität von 100 oder weniger:

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

Prioritäten erfordern das sogenannte journal.

Löschen des Caches

Der Parameter Cache::All löscht alles:

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

Massenlesen (Bulk Read)

Für das Massenlesen 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 dem zweiten Parameter, einem Callback, dem der Schlüssel des generierten Elements übergeben wird:

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

Verwendung mit PSR-16

Zur Verwendung von Nette Cache mit der PSR-16-Schnittstelle können Sie den Adapter Nette\Bridges\Psr\PsrCacheAdapter nutzen. Er ermöglicht eine nahtlose Integration zwischen Nette Cache und jedem Code oder jeder Bibliothek, die einen PSR-16-kompatiblen Cache erwartet.

$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

Die Ausgabe kann sehr elegant abgefangen und gecached werden:

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

	echo ... // wir geben Daten aus

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

Falls die Ausgabe bereits im Cache gespeichert ist, gibt die Methode capture() sie aus und gibt null zurück, sodass die Bedingung nicht ausgeführt wird. Andernfalls beginnt sie mit dem Abfangen der Ausgabe und gibt das Objekt $capture zurück, mit dessen Hilfe wir die ausgegebenen Daten schließlich im Cache speichern.

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

Caching in Latte

Das Caching in Latte-Templates ist sehr einfach, es genügt, einen Teil des Templates mit den Tags {cache}...{/cache} zu umschließen. Der Cache wird automatisch invalidiert, sobald sich das Quelltemplate ändert (einschließlich eventuell eingebundener Templates innerhalb des Cache-Blocks). Die {cache}-Tags können ineinander verschachtelt werden, und wenn ein verschachtelter Block ungültig wird (z. B. durch ein Tag), wird auch der übergeordnete Block ungültig.

Im Tag können Schlüssel angegeben werden, an die der Cache gebunden wird (hier die Variable $id), sowie der Ablauf und Tags zur Invalidierung eingestellt werden.

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

Alle Parameter sind optional, sodass wir weder den Ablauf noch die Tags und letztendlich nicht einmal die Schlüssel angeben müssen.

Die Verwendung des Caches kann auch mit if bedingt werden – der Inhalt wird dann nur gecached, wenn die Bedingung erfüllt ist:

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

Speicher (Storage)

Ein Speicher (Storage) ist ein Objekt, das den Ort darstellt, an dem Daten physisch gespeichert werden. Wir können eine Datenbank, einen Memcached-Server oder den am leichtesten verfügbaren Speicher verwenden, nämlich Dateien auf der Festplatte.

Speicher Beschreibung
FileStorage Standardspeicher mit Speicherung in Dateien auf der Festplatte
MemcachedStorage verwendet einen Memcached-Server
MemoryStorage Daten werden temporär im Speicher gehalten
SQLiteStorage Daten werden in einer SQLite-Datenbank gespeichert
DevNullStorage Daten werden nicht gespeichert, geeignet zum Testen

Zum Speicherobjekt gelangen Sie, indem Sie es sich mittels Dependency Injection mit dem Typ Nette\Caching\Storage übergeben lassen. Als Standardspeicher stellt Nette das FileStorage-Objekt bereit, das Daten im Unterordner cache im Verzeichnis für temporäre Dateien speichert.

Den Speicher können Sie in der Konfiguration ändern:

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

FileStorage

Schreibt den Cache in Dateien auf der Festplatte. Der Speicher Nette\Caching\Storages\FileStorage ist sehr gut für die Leistung optimiert und gewährleistet vor allem die volle Atomizität der Operationen. Was bedeutet das? Dass bei Verwendung des Caches nicht passieren kann, dass wir eine Datei lesen, die von einem anderen Thread noch nicht vollständig geschrieben wurde, oder dass sie jemand „unter den Händen“ löscht. Die Verwendung des Caches ist also absolut sicher.

Dieser Speicher verfügt auch über eine wichtige integrierte Funktion, die einen extremen Anstieg der CPU-Auslastung verhindert, wenn der Cache gelöscht wird oder noch nicht „aufgewärmt“ (d.h. erstellt) ist. Dies ist eine Prävention gegen den Cache Stampede. Es kommt vor, dass zu einem Zeitpunkt eine größere Anzahl gleichzeitiger Anfragen eingeht, die dasselbe Element aus dem Cache abrufen möchten (z. B. das Ergebnis einer teuren SQL-Abfrage), und da es nicht im Cache vorhanden ist, beginnen alle Prozesse, dieselbe SQL-Abfrage auszuführen. Die Auslastung vervielfacht sich, und es kann sogar vorkommen, dass kein Thread innerhalb des Zeitlimits antworten kann, der Cache nicht erstellt wird und die Anwendung zusammenbricht. Glücklicherweise funktioniert der Cache in Nette so, dass bei mehreren gleichzeitigen Anfragen für ein Element nur der erste Thread dieses generiert, die anderen warten und anschließend das generierte Ergebnis verwenden.

Beispiel für die Erstellung von FileStorage:

// Der Speicher wird das Verzeichnis '/path/to/temp' auf der Festplatte sein
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

Der Memcached-Server ist ein hochleistungsfähiges verteiltes In-Memory-Speichersystem, dessen Adapter Nette\Caching\Storages\MemcachedStorage ist. In der Konfiguration geben wir die IP-Adresse und den Port an, falls dieser vom Standardport 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 Speicher, der Daten in einem PHP-Array ablegt und somit mit dem Ende der Anfrage verloren gehen.

SQLiteStorage

Die SQLite-Datenbank und der Adapter Nette\Caching\Storages\SQLiteStorage bieten eine Möglichkeit, den Cache in einer einzigen Datei auf der Festplatte zu speichern. 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 spezielle Implementierung des Speichers ist Nette\Caching\Storages\DevNullStorage, das tatsächlich überhaupt keine Daten speichert. Es eignet sich daher zum Testen, wenn wir den Einfluss des Caches eliminieren möchten.

Verwendung des Caches im Code

Bei der Verwendung des Caches im Code gibt es zwei Möglichkeiten. Die erste besteht darin, sich den Speicher mittels Dependency Injection übergeben zu lassen und ein Cache-Objekt zu erstellen:

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 besteht darin, sich direkt das Cache-Objekt übergeben zu lassen:

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

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

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

Journal

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

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

services:
	cache.journal: MyJournal

DI-Dienste

Diese Dienste werden dem DI-Container hinzugefügt:

Name Typ Beschreibung
cache.journal Nette\Caching\Storages\Journal Journal
cache.storage Nette\Caching\Storage Speicher

Deaktivieren des Caches

Eine Möglichkeit, den Cache in der Anwendung zu deaktivieren, besteht darin, DevNullStorage als Speicher festzulegen:

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

Diese Einstellung hat keinen Einfluss auf das Caching von Templates in Latte oder des DI-Containers, da diese Bibliotheken die Dienste von nette/caching nicht nutzen und ihren Cache selbst verwalten. Ihr Cache muss im Entwicklermodus übrigens nicht deaktiviert werden.

Version: 3.x