Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Cache

La cache ([keš]) accelera la tua applicazione salvando i dati ottenuti con fatica una volta per un uso futuro. Vediamo:

  • come usare la cache
  • come cambiare lo storage
  • come invalidare correttamente la cache

L'uso della cache in Nette è molto semplice, ma copre anche esigenze molto avanzate. È progettato per le prestazioni e una resistenza del 100%. Di base, troverai adattatori per gli storage di backend più comuni. Permette l'invalidazione basata su tag, la scadenza temporale, ha protezione contro il cache stampede, ecc.

Installazione

Potete scaricare e installare la libreria utilizzando lo strumento Composer:

composer require nette/caching

Utilizzo di base

Il fulcro del lavoro con la cache è l'oggetto Nette\Caching\Cache. Creiamo la sua istanza e passiamo al costruttore il cosiddetto storage come parametro. Questo è un oggetto che rappresenta il luogo in cui i dati verranno fisicamente salvati (database, Memcached, file su disco, …). Possiamo accedere allo storage facendocelo passare tramite dependency injection con il tipo Nette\Caching\Storage. Troverai tutto l'essenziale nella sezione Storage.

Nella versione 3.0, l'interfaccia aveva ancora il prefisso I, quindi il nome era Nette\Caching\IStorage. Inoltre, le costanti della classe Cache erano scritte in maiuscolo, quindi ad esempio Cache::EXPIRE invece di Cache::Expire.

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

use Nette\Caching\Cache;

$storage = /* ... */; // instance of Nette\Caching\Storage

La cache è essenzialmente un key-value store, quindi leggiamo e scriviamo dati sotto chiavi proprio come con gli array associativi. Le applicazioni sono composte da una serie di parti indipendenti e se tutte utilizzassero un unico storage (immagina una singola directory su disco), prima o poi si verificherebbe una collisione di chiavi. Nette Framework risolve il problema dividendo l'intero spazio in namespace (sottodirectory). Ogni parte del programma utilizza quindi il proprio spazio con un nome univoco e non può più verificarsi alcuna collisione.

Il nome dello spazio è 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. A tal fine serve il metodo load(). Il primo argomento è la chiave e il secondo è un callback PHP che viene chiamato quando la chiave non viene trovata nella cache. Il callback genera il valore, lo restituisce e viene salvato nella cache:

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

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

Fantastico è che nella cache è possibile salvare qualsiasi struttura serializzabile, non devono essere solo stringhe. E lo stesso vale anche per le chiavi.

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

$cache->remove($key);

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

Memoizzazione

La memoizzazione significa memorizzare nella cache il risultato di una chiamata a una funzione o a un metodo, in modo da poterlo utilizzare la prossima volta senza calcolare nuovamente la stessa cosa.

È possibile chiamare metodi e funzioni in modo memoizzato usando call(callable $callback, ...$args):

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

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

È anche possibile creare un wrapper memoizzato su un metodo o una funzione che può essere chiamato in seguito:

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

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

$result = $memoizedFactorial(5); // calcola la prima volta
$result = $memoizedFactorial(5); // la seconda volta dalla cache

Scadenza & Invalidazione

Con il salvataggio nella cache, è necessario risolvere la questione di quando i dati precedentemente salvati diventano non validi. Nette Framework offre un meccanismo per limitare la validità dei dati o cancellarli in modo controllato (nella terminologia del framework “invalidare”).

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

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

Oppure tramite il parametro $dependencies passato per riferimento al callback del metodo load(), ad esempio:

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

Oppure tramite il 3° parametro nel metodo load(), ad esempio:

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

Negli esempi successivi, assumeremo la seconda variante e quindi l'esistenza della variabile $dependencies.

Scadenza

La scadenza più semplice è un limite di tempo. In questo modo salviamo nella cache i dati con validità di 20 minuti:

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

Se volessimo estendere il periodo di validità ad ogni lettura, è possibile farlo nel modo seguente, ma attenzione, il sovraccarico della cache aumenterà:

$dependencies[Cache::Sliding] = true;

È utile la possibilità di far scadere i dati nel momento in cui cambia un file o uno dei più file. Questo può essere utilizzato, ad esempio, per salvare nella cache i dati derivanti dall'elaborazione 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'];

Possiamo far scadere un elemento nella cache nel momento in cui scade un altro elemento (o uno dei più altri). Questo può essere utilizzato quando salviamo nella cache, ad esempio, un'intera pagina HTML e, sotto altre chiavi, i suoi frammenti. Non appena un frammento cambia, l'intera pagina viene invalidata. Se abbiamo i frammenti salvati sotto le chiavi, ad esempio frag1 e frag2, useremo:

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

La scadenza può essere controllata anche tramite funzioni personalizzate o metodi statici, che decidono sempre alla lettura se l'elemento è ancora valido. In questo modo, ad esempio, possiamo far scadere l'elemento ogni volta che cambia la versione di PHP. Creiamo una funzione che confronta la versione attuale con il parametro e, durante il salvataggio, aggiungiamo tra le dipendenze un array nella forma [nome funzione, ...argomenti]:

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

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

Ovviamente è possibile combinare tutti i criteri. La cache scadrà quindi quando almeno un criterio non è soddisfatto.

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

Invalidazione tramite tag

Uno strumento di invalidazione molto utile sono i cosiddetti tag. A ogni elemento nella cache possiamo assegnare un elenco di tag, che sono stringhe arbitrarie. Supponiamo di avere una pagina HTML con un articolo e commenti, che memorizzeremo nella cache. Durante il salvataggio, specifichiamo i tag:

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

Spostiamoci nell'amministrazione. Qui troviamo un form per modificare l'articolo. Insieme al salvataggio dell'articolo nel database, chiamiamo il comando clean(), che elimina gli elementi dalla cache in base al tag:

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

Allo stesso modo, nel punto di aggiunta di un nuovo commento (o modifica di un commento), non dimentichiamo di invalidare il tag corrispondente:

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

Cosa abbiamo ottenuto con questo? Che la nostra cache HTML verrà invalidata (cancellata) ogni volta che l'articolo o i commenti cambiano. Quando viene modificato l'articolo con ID = 10, viene forzata l'invalidazione del tag article/10 e la pagina HTML che porta il tag specificato viene eliminata dalla cache. Lo stesso accade quando viene inserito un nuovo commento sotto l'articolo corrispondente.

I tag richiedono il cosiddetto Journal.

Invalidazione tramite priorità

Ai singoli elementi nella cache possiamo impostare una priorità, tramite la quale sarà possibile eliminarli, ad esempio, quando la cache supera una certa dimensione:

$dependencies[Cache::Priority] = 50;

Eliminiamo tutti gli elementi con priorità pari o inferiore a 100:

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

Le priorità richiedono il cosiddetto Journal.

Cancellazione della cache

Il parametro Cache::All cancella tutto:

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

Lettura di massa

Per la lettura e la scrittura di massa nella cache serve il metodo bulkLoad(), al quale passiamo un array di chiavi e otteniamo un array di valori:

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

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

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

Utilizzo con PSR-16

Per utilizzare Nette Cache con l'interfaccia PSR-16, puoi utilizzare l'adattatore PsrCacheAdapter. Consente un'integrazione senza soluzione di continuità tra Nette Cache e qualsiasi codice o libreria che si aspetta una cache compatibile con PSR-16.

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

Ora puoi usare $psrCache come cache PSR-16:

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

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

Caching dell'output

È possibile catturare e memorizzare nella cache l'output in modo molto elegante:

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

	echo ... // stampiamo i dati

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

Nel caso in cui l'output sia già memorizzato nella cache, il metodo capture() lo stamperà e restituirà null, quindi la condizione non verrà eseguita. In caso contrario, inizierà a catturare l'output e restituirà l'oggetto $capture, tramite il quale infine salveremo i dati stampati nella cache.

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

Caching in Latte

Il caching nei template Latte è molto semplice, basta racchiudere una parte del template con i tag {cache}...{/cache}. La cache viene invalidata automaticamente nel momento in cui cambia il template sorgente (inclusi eventuali template inclusi all'interno del blocco cache). I tag {cache} possono essere annidati e quando un blocco annidato viene invalidato (ad esempio tramite un tag), viene invalidato anche il blocco genitore.

Nel tag è possibile specificare le chiavi a cui la cache sarà legata (qui la variabile $id) e impostare la scadenza e i tag per l'invalidazione.

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

Tutti gli elementi sono opzionali, quindi non dobbiamo specificare né la scadenza, né i tag, e infine nemmeno le chiavi.

L'uso della cache può anche essere condizionato tramite if – il contenuto verrà quindi memorizzato nella cache solo se la condizione è soddisfatta:

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

Storage

Lo storage è un oggetto che rappresenta il luogo in cui i dati vengono fisicamente salvati. Possiamo usare un database, un server Memcached, o lo storage più accessibile, che sono i file su disco.

Storage Descrizione
FileStorage storage predefinito con salvataggio in file su disco
MemcachedStorage utilizza il server Memcached
MemoryStorage i dati sono temporaneamente in memoria
SQLiteStorage i dati vengono salvati in un database SQLite
DevNullStorage i dati non vengono salvati, adatto per i test

Si accede all'oggetto storage facendoselo passare tramite dependency injection con il tipo Nette\Caching\Storage. Come storage predefinito, Nette fornisce l'oggetto FileStorage che salva i dati nella sottodirectory cache nella directory per i 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. Cosa significa? Che quando si utilizza la cache, non può accadere di leggere un file che non è ancora stato completamente scritto da un altro thread, o che qualcuno te lo cancelli “sotto il naso”. L'uso della cache è quindi completamente sicuro.

Questo storage ha anche una funzione importante integrata che previene un aumento estremo dell'utilizzo della CPU nel momento in cui la cache viene cancellata o non è ancora “calda” (cioè creata). Si tratta di una prevenzione contro il cache stampede. Succede che, in un dato momento, un gran numero di richieste simultanee arrivino, chiedendo la stessa cosa dalla cache (ad esempio, il risultato di una costosa query SQL) e poiché non è nella cache, tutti i processi iniziano a eseguire la stessa query SQL. Il carico si moltiplica e può persino accadere che nessun thread riesca a rispondere entro il limite di tempo, la cache non viene creata e l'applicazione collassa. Fortunatamente, la cache in Nette funziona in modo tale che, in caso di più richieste simultanee per un singolo elemento, viene generato solo dal primo thread, gli altri aspettano e successivamente utilizzano il risultato generato.

Esempio di creazione di FileStorage:

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

MemcachedStorage

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

Richiede l'estensione PHP memcached.

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

MemoryStorage

Nette\Caching\Storages\MemoryStorage è uno storage che salva i dati in un array PHP, e quindi si perdono alla fine della richiesta.

SQLiteStorage

Il database SQLite e l'adattatore Nette\Caching\Storages\SQLiteStorage offrono un modo per salvare la cache in un unico file su disco. Nella configurazione, specifichiamo 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 dello storage è Nette\Caching\Storages\DevNullStorage, che in realtà non salva affatto i dati. È quindi adatto per i test, quando vogliamo eliminare l'influenza della cache.

Utilizzo della cache nel codice

Quando si utilizza la cache nel codice, abbiamo due modi per farlo. Il primo è farsi passare lo storage tramite dependency injection e creare l'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');
	}
}

La seconda opzione è farsi passare direttamente l'oggetto Cache:

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

L'oggetto Cache viene quindi creato direttamente nella configurazione in questo modo:

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

Journal

Nette salva i tag e le priorità nel cosiddetto journal. Standardmente, per questo viene utilizzato SQLite e il file journal.s3db e sono richieste le estensioni PHP pdo e pdo_sqlite.

È possibile modificare il journal nella configurazione:

services:
	cache.journal: MyJournal

Servizi DI

Questi servizi vengono aggiunti al container DI:

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

Disattivazione della cache

Una delle opzioni per disattivare la cache nell'applicazione è impostare DevNullStorage come storage:

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

Questa impostazione non influisce sulla cache dei template in Latte o del container DI, poiché queste librerie non utilizzano i servizi di nette/caching e gestiscono la cache autonomamente. La loro cache, del resto, non è necessario disattivare in modalità sviluppatore.

versione: 3.x