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.