Caching

La cache accelera l'applicazione memorizzando i dati, una volta recuperati, per un uso futuro. Vi mostreremo:

  • Come utilizzare la cache
  • Come modificare la memorizzazione nella cache
  • Come invalidare correttamente la cache

L'uso della cache è molto semplice in Nette, ma copre anche esigenze di cache molto avanzate. È stato progettato per garantire prestazioni e durata al 100%. Di base, si trovano adattatori per i più comuni storage di backend. Consente l'invalidazione basata sui tag, la protezione della cache, la scadenza temporale, ecc.

Installazione

Scaricare e installare il pacchetto utilizzando Composer:

composer require nette/caching

Uso di base

Il centro del lavoro con la cache è l'oggetto Nette\Caching\Cache. Si crea la sua istanza e si passa al costruttore come parametro il cosiddetto storage. Si tratta di un oggetto che rappresenta il luogo in cui i dati saranno fisicamente memorizzati (database, Memcached, file su disco, …). Si ottiene l'oggetto storage passandoglielo tramite dependency injection con il tipo Nette\Caching\Storage. Si troveranno tutti gli elementi essenziali nella sezione Storage.

Nella versione 3.0, l'interfaccia aveva ancora il tipo I prefix, so the name was Nette\Caching\IStorage. Inoltre, le costanti della classe Cache erano maiuscole, quindi ad esempio Cache::EXPIRE invece di Cache::Expire.

Per gli esempi seguenti, supponiamo di avere un alias Cache e una memoria nella variabile $storage.

use Nette\Caching\Cache;

$storage = /* ... */; // istanza di Nette\Caching\Storage

La cache è in realtà un magazzino chiave-valore, quindi leggiamo e scriviamo i dati sotto le chiavi proprio come gli array associativi. Le applicazioni sono costituite da un certo numero di parti indipendenti e se tutte utilizzassero un unico archivio (ad esempio una directory su disco), prima o poi si verificherebbe una collisione di chiavi. Il framework Nette risolve il problema dividendo l'intero spazio in spazi dei nomi (sottodirectory). Ogni parte del programma utilizza quindi il proprio spazio con un nome unico e non si verificano collisioni.

Il nome dello spazio viene specificato come secondo parametro del costruttore della classe Cache:

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

Ora possiamo usare l'oggetto $cache per leggere e scrivere dalla cache. Il metodo load() viene utilizzato per entrambi. Il primo parametro è la chiave e il secondo è il callback di PHP, che viene chiamato quando la chiave non viene trovata nella cache. La callback genera un valore, lo restituisce e lo mette in cache:

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

Se il secondo parametro non è specificato $value = $cache->load($key), viene restituito null se l'elemento non è presente nella cache.

Il bello è che qualsiasi struttura serializzabile può essere messa in cache, non solo le stringhe. Lo stesso vale per le chiavi.

L'elemento viene cancellato dalla cache con il metodo remove():

$cache->remove($key);

È possibile memorizzare nella cache un elemento anche con il metodo $cache->save($key, $value, array $dependencies = []). Tuttavia, è preferibile utilizzare il metodo load().

Memorizzazione

Memorizzazione significa memorizzare il risultato di una funzione o di un metodo, in modo da poterlo utilizzare la volta successiva invece di calcolare sempre la stessa cosa.

I metodi e le funzioni possono essere chiamati memoizzati usando call(callable $callback, ...$args):

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

La funzione gethostbyaddr() viene chiamata una sola volta per ogni parametro $ip e la volta successiva verrà restituito il valore dalla cache.

È anche possibile creare un wrapper memoizzato per un metodo o una funzione che può essere richiamato successivamente:

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

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

$result = $memoizedFactorial(5); // lo conta
$result = $memoizedFactorial(5); // lo restituisce dalla cache

Scadenza e invalidazione

Con la cache, è necessario affrontare il problema che alcuni dei dati precedentemente salvati diventino non validi nel tempo. Nette Framework fornisce un meccanismo per limitare la validità dei dati e per cancellarli in modo controllato (“invalidarli”, secondo la terminologia del framework).

La validità dei dati viene impostata al momento del salvataggio utilizzando il terzo parametro del metodo save(), ad esempio:

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

Oppure utilizzando il parametro $dependencies passato come riferimento al callback del metodo load(), ad es:

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

Oppure utilizzando il terzo parametro del metodo load(), ad es:

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

Negli esempi seguenti, assumeremo la seconda variante e quindi l'esistenza di una variabile $dependencies.

Scadenza

La scadenza più semplice è quella temporale. Ecco come memorizzare nella cache dati validi per 20 minuti:

// accetta anche il numero di secondi o il timestamp UNIX
$dependencies[Cache::Expire] = '20 minutes';

Se si vuole estendere il periodo di validità a ogni lettura, è possibile farlo in questo modo, ma attenzione, questo aumenterà l'overhead della cache:

$dependencies[Cache::Sliding] = true;

L'opzione più comoda è la possibilità di far scadere i dati quando un particolare file viene modificato o uno di più file. Questo può essere utilizzato, ad esempio, per la memorizzazione nella cache dei dati risultanti dalla processione di questi file. Utilizzare percorsi assoluti.

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

È possibile far scadere un elemento della cache quando un altro elemento (o uno di diversi altri) scade. Questo può essere usato quando si mette in cache l'intera pagina HTML e frammenti di essa sotto altre chiavi. Quando lo snippet cambia, l'intera pagina diventa non valida. Se abbiamo frammenti memorizzati sotto chiavi come frag1 e frag2, useremo:

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

La scadenza può anche essere controllata usando funzioni personalizzate o metodi statici, che decidono sempre al momento della lettura se l'elemento è ancora valido. Per esempio, possiamo far scadere l'elemento ogni volta che la versione di PHP cambia. Creeremo una funzione che confronta la versione corrente con il parametro e al momento del salvataggio aggiungeremo un array nella forma [function name, ...arguments] alle dipendenze:

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

$dependencies[Cache::Callbacks] = [
	['checkPhpVersion', PHP_VERSION_ID] // scadono quando checkPhpVersion(...) === false
];

Naturalmente, tutti i criteri possono essere combinati. La cache scade quando almeno un criterio non è soddisfatto.

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

Invalidazione tramite tag

I tag sono uno strumento di invalidazione molto utile. Si può assegnare un elenco di tag, che sono stringhe arbitrarie, a ogni elemento memorizzato nella cache. Per esempio, supponiamo di avere una pagina HTML con un articolo e dei commenti, che vogliamo mettere in cache. Quindi specifichiamo i tag quando salviamo nella cache:

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

Passiamo ora all'amministrazione. Qui abbiamo un modulo per la modifica degli articoli. Insieme al salvataggio dell'articolo nel database, chiamiamo il comando clean(), che cancellerà gli articoli in cache per tag:

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

Allo stesso modo, quando aggiungiamo un nuovo commento (o modifichiamo un commento), non dimentichiamo di invalidare il relativo tag:

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

Cosa abbiamo ottenuto? Che la nostra cache HTML sarà invalidata (cancellata) ogni volta che l'articolo o i commenti cambiano. Quando si modifica un articolo con ID = 10, il tag article/10 viene forzatamente invalidato e la pagina HTML che contiene il tag viene cancellata dalla cache. Lo stesso accade quando si inserisce un nuovo commento nell'articolo in questione.

I tag richiedono Journal.

Invalidazione per priorità

Possiamo impostare la priorità dei singoli elementi della cache e sarà possibile eliminarli in modo controllato quando, ad esempio, la cache supera una certa dimensione:

$dependencies[Cache::Priority] = 50;

Elimina tutti gli elementi con una priorità uguale o inferiore a 100:

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

Le priorità richiedono il cosiddetto Journal.

Cancellare la cache

Il parametro Cache::All cancella tutto:

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

Lettura massiva

Per la lettura e la scrittura in massa nella cache, si utilizza il metodo bulkLoad(), in cui si passa un array di chiavi e si ottiene un array di valori:

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

Il metodo bulkLoad() funziona in modo simile a load() con il secondo parametro di callback, al quale viene passata la chiave dell'elemento generato:

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

Utilizzo con PSR-16

Per utilizzare Nette Cache con l'interfaccia PSR-16, è possibile utilizzare il programma PsrCacheAdapter. Esso consente una perfetta integrazione tra Nette Cache e qualsiasi codice o libreria che preveda una cache compatibile con PSR-16.

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

Ora è possibile utilizzare $psrCache come cache PSR-16:

$psrCache->set('key', 'value', 3600); // memorizza il valore per 1 ora
$value = $psrCache->get('key', 'default');

L'adattatore supporta tutti i metodi definiti in PSR-16, compresi getMultiple(), setMultiple() e deleteMultiple().

Caching dell'output

L'output può essere catturato e messo in cache in modo molto elegante:

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

	echo ... // stampa alcuni dati

	$capture->end(); // salva l'output nella cache
}

Nel caso in cui l'output sia già presente nella cache, il metodo capture() lo stampa e restituisce null, quindi la condizione non verrà eseguita. Altrimenti, inizia a bufferizzare l'output e restituisce l'oggetto $capture, con il quale si salvano finalmente i dati nella cache.

Nella versione 3.0 il metodo si chiamava $cache->start().

La cache in Latte

La cache nei template di Latte è molto semplice, basta avvolgere parte del template con i tag {cache}...{/cache}. La cache viene automaticamente invalidata quando il modello sorgente cambia (compresi i modelli inclusi nei tag {cache} ). I tag {cache} possono essere annidati e quando un blocco annidato viene invalidato (per esempio da un tag), anche il blocco padre viene invalidato.

Nel tag è possibile specificare le chiavi a cui sarà legata la cache (in questo caso la variabile $id) e impostare i tag di scadenza e invalidazione

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

Tutti i parametri sono opzionali, quindi non è necessario specificare scadenza, tag o chiavi.

L'uso della cache può anche essere condizionato da if – il contenuto sarà messo in cache solo se la condizione è soddisfatta:

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

Archivi

Uno storage è un oggetto che rappresenta il luogo in cui i dati vengono fisicamente memorizzati. Si può usare un database, un server Memcached o lo storage più disponibile, ovvero i file su disco.

Memorizzazione Descrizione
FileStorage archiviazione predefinita con salvataggio su file su disco
MemcachedStorage utilizza il server Memcached
MemoryStorage i dati sono temporaneamente in memoria
SQLiteStorage i dati sono memorizzati nel database SQLite
DevNullStorage i dati non sono memorizzati – a scopo di test

Si ottiene l'oggetto storage passandoglielo tramite dependency injection con il tipo Nette\Caching\Storage. Per impostazione predefinita, Nette fornisce un oggetto FileStorage che memorizza i dati in una sottocartella cache nella directory dei file temporanei.

È possibile modificare lo storage nella configurazione:

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

FileStorage

Scrive la cache in file su disco. Lo storage Nette\Caching\Storages\FileStorage è molto ben ottimizzato per le prestazioni e soprattutto garantisce la piena atomicità delle operazioni. Che cosa significa? Che quando si utilizza la cache, non può accadere che si legga un file che non è stato ancora completamente scritto da un altro thread, o che qualcuno lo cancelli “sotto le sue mani”. L'uso della cache è quindi completamente sicuro.

Questa memoria ha anche un'importante funzione integrata che impedisce un aumento estremo dell'utilizzo della CPU quando la cache viene cancellata o raffreddata (cioè non creata). Si tratta della prevenzione del cache stampede. Succede che in un momento ci sono diverse richieste concorrenti che vogliono la stessa cosa dalla cache (ad esempio il risultato di una query SQL costosa) e poiché non è presente nella cache, tutti i processi iniziano a eseguire la stessa query SQL. Il carico del processore si moltiplica e può anche accadere che nessun thread riesca a rispondere entro il tempo limite, la cache non viene creata e l'applicazione si blocca. Fortunatamente, la cache in Nette funziona in modo tale che quando ci sono più richieste simultanee per un elemento, questo viene generato solo dal primo thread, gli altri aspettano e poi usano il risultato generato.

Esempio di creazione di un FileStorage:

// lo storage sarà la directory '/path/to/temp' sul disco
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

Il server Memcached è un sistema di archiviazione distribuita ad alte prestazioni il cui adattatore è Nette\Caching\Storages\MemcachedStorage. Nella configurazione, specificare l'indirizzo IP e la porta se differisce dallo standard 11211.

Richiede l'estensione PHP memcached.

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

MemoriaStorage

Nette\Caching\Storages\MemoryStorage è una memoria che memorizza i dati in un array PHP e che quindi viene persa quando la richiesta viene terminata.

SQLiteStorage

Il database SQLite e l'adattatore Nette\Caching\Storages\SQLiteStorage offrono un modo per memorizzare la cache in un singolo file su disco. La configurazione specificherà il percorso di questo file.

Richiede le estensioni PHP pdo e pdo_sqlite.

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

DevNullStorage

Un'implementazione speciale di storage è Nette\Caching\Storages\DevNullStorage, che non memorizza affatto i dati. È quindi adatta per i test se si vuole eliminare l'effetto della cache.

Utilizzo della cache nel codice

Quando si usa la cache nel codice, ci sono due modi per farlo. Il primo è quello di ottenere l'oggetto di memorizzazione passandoglielo tramite dependency injection e poi creare un oggetto Cache:

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');
	}
}

Il secondo modo è quello di ottenere l'oggetto di memorizzazione Cache:

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

L'oggetto Cache viene quindi creato direttamente nella configurazione come segue:

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

Giornale

Nette memorizza i tag e le priorità in un cosiddetto journal. Per impostazione predefinita, vengono utilizzati SQLite e il file journal.s3db, mentre sono necessarie le estensioni **PHP pdo e pdo_sqlite **.

È possibile modificare il diario nella configurazione:

services:
	cache.journal: MyJournal

Servizi DI

Questi servizi vengono aggiunti al contenitore DI:

Nome Tipo Descrizione
cache.journal Nette\Caching\Storages\Journal journal
cache.storage Nette\Caching\Storage repository

Disattivare la cache

Uno dei modi per disattivare la cache nell'applicazione è impostare lo storage su DevNullStorage:

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

Questa impostazione non influisce sulla cache dei modelli in Latte o nel contenitore DI, poiché queste librerie non utilizzano i servizi di nette/caching e gestiscono la loro cache in modo indipendente. Inoltre, la loro cache non deve essere disattivata in modalità di sviluppo.

versione: 3.x