Cache
Cache-ul [keš]
accelerează aplicația dvs. salvând datele obținute cu efort pentru utilizare ulterioară.
Vom arăta:
- cum să utilizați cache-ul
- cum să schimbați stocarea
- cum să invalidați corect cache-ul
Utilizarea cache-ului în Nette este foarte ușoară, acoperind în același timp și nevoi foarte avansate. Este proiectat pentru performanță și rezistență 100%. În mod implicit, veți găsi adaptoare pentru cele mai comune stocări backend. Permite invalidarea bazată pe tag-uri, expirarea în timp, are protecție împotriva cache stampede etc.
Instalare
Descărcați și instalați biblioteca folosind Composer:
composer require nette/caching
Utilizare de bază
Centrul lucrului cu cache-ul este obiectul Nette\Caching\Cache. Creăm o instanță a acestuia și
îi transmitem constructorului așa-numita stocare (storage). Acesta este un obiect care reprezintă locul unde datele vor fi
stocate fizic (bază de date, Memcached, fișiere pe disc, …). Ajungem la stocare lăsându-ne să o primim prin dependency injection cu tipul
Nette\Caching\Storage
. Veți afla tot ce este esențial în secțiunea Stocări.
În versiunea 3.0, interfața avea încă prefixul I
, deci numele era
Nette\Caching\IStorage
. De asemenea, constantele clasei Cache
erau scrise cu majuscule, deci, de
exemplu, Cache::EXPIRE
în loc de Cache::Expire
.
Pentru următoarele exemple, presupunem că avem un alias Cache
creat și stocarea în variabila
$storage
.
use Nette\Caching\Cache;
$storage = /* ... */; // instanță de Nette\Caching\Storage
Cache-ul este de fapt un key–value store, adică citim și scriem date sub chei la fel ca în array-urile asociative. Aplicațiile sunt compuse din mai multe părți independente și dacă toate ar folosi o singură stocare (imaginați-vă un singur director pe disc), mai devreme sau mai târziu ar apărea o coliziune de chei. Nette Framework rezolvă problema împărțind întregul spațiu în spații de nume (subdirectoare). Fiecare parte a programului folosește apoi propriul spațiu cu un nume unic și nu mai poate apărea nicio coliziune.
Numele spațiului îl specificăm ca al doilea parametru al constructorului clasei Cache:
$cache = new Cache($storage, 'Full Html Pages');
Acum putem folosi obiectul $cache
pentru a citi și scrie în cache. Pentru ambele servește metoda
load()
. Primul argument este cheia și al doilea este un callback PHP, care este apelat atunci când cheia nu este
găsită în cache. Callback-ul generează valoarea, o returnează și aceasta este salvată în cache:
$value = $cache->load($key, function () use ($key) {
$computedValue = /* ... */; // calcul costisitor
return $computedValue;
});
Dacă nu specificăm al doilea parametru $value = $cache->load($key)
, se va returna null
dacă
elementul nu este în cache.
Este grozav că în cache pot fi stocate orice structuri serializabile, nu trebuie să fie doar șiruri de caractere. Și același lucru este valabil chiar și pentru chei.
Elementul din cache îl ștergem cu metoda remove()
:
$cache->remove($key);
Salvarea unui element în cache se poate face și cu metoda
$cache->save($key, $value, array $dependencies = [])
. Cu toate acestea, metoda preferată este cea menționată
mai sus, folosind load()
.
Memoizare
Memoizarea înseamnă stocarea în cache a rezultatului apelării unei funcții sau metode, astfel încât să îl puteți utiliza data viitoare fără a calcula același lucru din nou și din nou.
Metodele și funcțiile pot fi apelate memoizat folosind call(callable $callback, ...$args)
:
$result = $cache->call('gethostbyaddr', $ip);
Funcția gethostbyaddr()
va fi astfel apelată pentru fiecare parametru $ip
o singură dată, iar
data viitoare se va returna valoarea din cache.
De asemenea, este posibil să creați un wrapper memoizat peste o metodă sau funcție, care poate fi apelat ulterior:
function factorial($num)
{
return /* ... */;
}
$memoizedFactorial = $cache->wrap('factorial');
$result = $memoizedFactorial(5); // calculează prima dată
$result = $memoizedFactorial(5); // a doua oară din cache
Expirare & invalidare
Cu stocarea în cache, trebuie rezolvată problema când datele stocate anterior devin invalide. Nette Framework oferă un mecanism pentru a limita validitatea datelor sau pentru a le șterge controlat (în terminologia framework-ului „a invalida”).
Validitatea datelor se setează în momentul salvării, folosind al treilea parametru al metodei save()
, de
exemplu:
$cache->save($key, $value, [
$cache::Expire => '20 minutes',
]);
Sau folosind parametrul $dependencies
transmis prin referință la callback-ul metodei load()
, de
exemplu:
$value = $cache->load($key, function (&$dependencies) {
$dependencies[Cache::Expire] = '20 minutes';
return /* ... */;
});
Sau folosind al treilea parametru în metoda load()
, de exemplu:
$value = $cache->load($key, function () {
return ...;
}, [Cache::Expire => '20 minutes']);
În următoarele exemple, vom presupune a doua variantă și, prin urmare, existența variabilei
$dependencies
.
Expirare
Cea mai simplă expirare este limita de timp. Astfel stocăm date în cache cu o valabilitate de 20 de minute:
// acceptă și numărul de secunde sau timestamp UNIX
$dependencies[Cache::Expire] = '20 minutes';
Dacă am dori să prelungim perioada de valabilitate la fiecare citire, se poate realiza astfel, dar atenție, costul cache-ului va crește:
$dependencies[Cache::Sliding] = true;
Este utilă posibilitatea de a lăsa datele să expire în momentul în care se modifică un fișier sau unul dintre mai multe fișiere. Acest lucru poate fi utilizat, de exemplu, la stocarea în cache a datelor rezultate din procesarea acestor fișiere. Utilizați căi absolute.
$dependencies[Cache::Files] = '/path/to/data.yaml';
// sau
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];
Putem lăsa un element din cache să expire în momentul în care expiră un alt element (sau unul dintre mai multe altele).
Acest lucru poate fi utilizat atunci când stocăm în cache, de exemplu, o întreagă pagină HTML și sub alte chei fragmentele
sale. De îndată ce un fragment se modifică, întreaga pagină este invalidată. Dacă fragmentele sunt stocate sub cheile, de
exemplu, frag1
și frag2
, folosim:
$dependencies[Cache::Items] = ['frag1', 'frag2'];
Expirarea poate fi controlată și prin funcții proprii sau metode statice, care decid întotdeauna la citire dacă elementul
este încă valid. Astfel, de exemplu, putem lăsa un element să expire ori de câte ori se schimbă versiunea PHP. Creăm
o funcție care compară versiunea curentă cu parametrul și, la salvare, adăugăm între dependențe un array de forma
[nume functie, ...argumente]
:
function checkPhpVersion($ver): bool
{
return $ver === PHP_VERSION_ID;
}
$dependencies[Cache::Callbacks] = [
['checkPhpVersion', PHP_VERSION_ID] // expiră când checkPhpVersion(...) === false
];
Toate criteriile pot fi, desigur, combinate. Cache-ul va expira atunci când cel puțin un criteriu nu este îndeplinit.
$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';
Invalidare prin tag-uri
Un instrument de invalidare foarte util sunt așa-numitele tag-uri. Fiecărui element din cache îi putem atribui la salvare o listă de tag-uri, care sunt șiruri de caractere arbitrare. Să avem, de exemplu, o pagină HTML cu un articol și comentarii, pe care o vom stoca în cache. La salvare, specificăm tag-urile:
$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];
Să ne mutăm în administrare. Aici găsim un formular pentru editarea articolului. Împreună cu salvarea articolului în
baza de date, vom apela comanda clean()
, care va șterge din cache elementele conform tag-ului:
$cache->clean([
$cache::Tags => ["article/$articleId"],
]);
La fel, în locul adăugării unui nou comentariu (sau editării unui comentariu), nu vom omite invalidarea tag-ului corespunzător:
$cache->clean([
$cache::Tags => ["comments/$articleId"],
]);
Ce am obținut prin asta? Că cache-ul nostru HTML se va invalida (șterge) ori de câte ori se modifică articolul sau
comentariile. Când se editează articolul cu ID = 10, se va forța invalidarea tag-ului article/10
și pagina HTML
care poartă tag-ul menționat se va șterge din cache. Același lucru se întâmplă la inserarea unui nou comentariu sub
articolul respectiv.
Tag-urile necesită așa-numitul Journal.
Invalidare prin prioritate
Fiecărui element din cache îi putem seta o prioritate, cu ajutorul căreia va fi posibil să le ștergem, de exemplu, când cache-ul depășește o anumită dimensiune:
$dependencies[Cache::Priority] = 50;
Ștergem toate elementele cu prioritate egală sau mai mică de 100:
$cache->clean([
$cache::Priority => 100,
]);
Prioritățile necesită așa-numitul Journal.
Ștergerea cache-ului
Parametrul Cache::All
șterge tot:
$cache->clean([
$cache::All => true,
]);
Citire în masă
Pentru citirea și scrierea în masă în cache servește metoda bulkLoad()
, căreia îi transmitem un array de
chei și obținem un array de valori:
$values = $cache->bulkLoad($keys);
Metoda bulkLoad()
funcționează similar cu load()
, inclusiv cu al doilea parametru callback, căruia
i se transmite cheia elementului generat:
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
$computedValue = /* ... */; // calcul costisitor
return $computedValue;
});
Utilizare cu PSR-16
Pentru a utiliza Nette Cache cu interfața PSR-16, puteți folosi adaptorul PsrCacheAdapter
. Acesta permite
integrarea fără probleme între Nette Cache și orice cod sau bibliotecă care așteaptă un cache compatibil PSR-16.
$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);
Acum puteți utiliza $psrCache
ca un cache PSR-16:
$psrCache->set('key', 'value', 3600); // salvează valoarea pentru 1 oră
$value = $psrCache->get('key', 'default');
Adaptorul suportă toate metodele definite în PSR-16, inclusiv getMultiple()
, setMultiple()
și
deleteMultiple()
.
Stocarea în cache a ieșirii
Se poate captura și stoca în cache ieșirea foarte elegant:
if ($capture = $cache->capture($key)) {
echo ... // afișăm date
$capture->end(); // salvăm ieșirea în cache
}
În cazul în care ieșirea este deja stocată în cache, metoda capture()
o va afișa și va returna
null
, deci condiția nu se va executa. În caz contrar, va începe să captureze ieșirea și va returna obiectul
$capture
, cu ajutorul căruia vom salva în final datele afișate în cache.
În versiunea 3.0, metoda se numea $cache->start()
.
Stocarea în cache în Latte
Stocarea în cache în șabloanele Latte este foarte ușoară, este suficient să
încadrați o parte a șablonului cu tag-urile {cache}...{/cache}
. Cache-ul se invalidează automat în momentul în
care se modifică șablonul sursă (inclusiv eventualele șabloane incluse în interiorul blocului cache). Tag-urile
{cache}
pot fi imbricate, iar când un bloc imbricat devine invalid (de exemplu, printr-un tag), blocul părinte
devine și el invalid.
În tag se pot specifica cheile de care va depinde cache-ul (aici variabila $id
) și se poate seta expirarea și
tag-urile pentru invalidare
{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
...
{/cache}
Toate elementele sunt opționale, deci nu trebuie să specificăm nici expirarea, nici tag-urile, și nici măcar cheile.
Utilizarea cache-ului poate fi, de asemenea, condiționată folosind if
– conținutul va fi stocat în cache
doar dacă condiția este îndeplinită:
{cache $id, if: !$form->isSubmitted()}
{$form}
{/cache}
Stocări
Stocarea este un obiect care reprezintă locul unde datele sunt stocate fizic. Putem folosi o bază de date, un server Memcached sau cea mai accesibilă stocare, care sunt fișierele pe disc.
Stocare | Descriere |
---|---|
FileStorage | stocare implicită cu salvare în fișiere pe disc |
MemcachedStorage | utilizează serverul Memcached |
MemoryStorage | datele sunt temporar în memorie |
SQLiteStorage | datele se salvează într-o bază de date SQLite |
DevNullStorage | datele nu se salvează, potrivit pentru testare |
La obiectul de stocare ajungeți lăsându-vă să vi-l transmită prin dependency injection cu tipul
Nette\Caching\Storage
. Ca stocare implicită, Nette oferă obiectul FileStorage care salvează datele în
subdirectorul cache
din directorul pentru fișiere temporare.
Puteți schimba stocarea în configurație:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
FileStorage
Scrie cache-ul în fișiere pe disc. Stocarea Nette\Caching\Storages\FileStorage
este foarte bine optimizată
pentru performanță și, mai presus de toate, asigură atomicitatea completă a operațiunilor. Ce înseamnă asta? Că la
utilizarea cache-ului nu se poate întâmpla să citim un fișier care nu a fost încă scris complet de un alt fir de execuție,
sau ca cineva să ni-l șteargă „sub nas”. Utilizarea cache-ului este, prin urmare, complet sigură.
Această stocare are, de asemenea, o funcție importantă încorporată, care previne creșterea extremă a utilizării CPU în momentul în care cache-ul este șters sau nu este încă încălzit (adică creat). Aceasta este o prevenire a „cache stampede”:https://en.wikipedia.org/…che_stampede. Se întâmplă ca, la un moment dat, să apară un număr mai mare de cereri concurente care doresc același lucru din cache (de exemplu, rezultatul unei interogări SQL costisitoare) și, deoarece nu se află în cache, toate procesele încep să execute aceeași interogare SQL. Sarcina se multiplică astfel și se poate chiar întâmpla ca niciun fir de execuție să nu reușească să răspundă în limita de timp, cache-ul să nu se creeze și aplicația să se prăbușească. Din fericire, cache-ul din Nette funcționează astfel încât, în cazul mai multor cereri concurente pentru un singur element, acesta este generat doar de primul fir de execuție, celelalte așteaptă și apoi utilizează rezultatul generat.
Exemplu de creare a FileStorage:
// stocarea va fi directorul '/path/to/temp' pe disc
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');
MemcachedStorage
Serverul Memcached este un sistem de înaltă performanță pentru stocarea în memorie
distribuită, al cărui adaptor este Nette\Caching\Storages\MemcachedStorage
. În configurație specificăm adresa IP
și portul, dacă diferă de cel standard 11211.
Necesită extensia PHP memcached
.
services:
cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')
MemoryStorage
Nette\Caching\Storages\MemoryStorage
este o stocare care salvează datele într-un array PHP și, prin urmare, se
pierd la terminarea cererii.
SQLiteStorage
Baza de date SQLite și adaptorul Nette\Caching\Storages\SQLiteStorage
oferă o modalitate de a stoca cache-ul
într-un singur fișier pe disc. În configurație specificăm calea către acest fișier.
Necesită extensiile PHP pdo
și pdo_sqlite
.
services:
cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')
DevNullStorage
O implementare specială a stocării este Nette\Caching\Storages\DevNullStorage
, care de fapt nu stochează deloc
datele. Este astfel potrivită pentru testare, când dorim să eliminăm influența cache-ului.
Utilizarea cache-ului în cod
La utilizarea cache-ului în cod, avem două moduri de a proceda. Primul este să ne lăsăm să primim stocarea prin dependency injection și să creăm obiectul
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');
}
}
A doua opțiune este să ne lăsăm să primim direct obiectul Cache
:
class ClassTwo
{
public function __construct(
private Nette\Caching\Cache $cache,
) {
}
}
Obiectul Cache
este apoi creat direct în configurație în acest mod:
services:
- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )
Journal
Nette stochează tag-urile și prioritățile în așa-numitul journal. În mod standard, se utilizează SQLite și fișierul
journal.s3db
și sunt necesare extensiile PHP pdo
și pdo_sqlite
.
Puteți schimba journal-ul în configurație:
services:
cache.journal: MyJournal
Servicii DI
Aceste servicii sunt adăugate în containerul DI:
Nume | Tip | Descriere |
---|---|---|
cache.journal |
Nette\Caching\Storages\Journal | journal |
cache.storage |
Nette\Caching\Storage | stocare |
Dezactivarea cache-ului
Una dintre opțiunile pentru a dezactiva cache-ul în aplicație este să setați ca stocare DevNullStorage:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
Această setare nu afectează stocarea în cache a șabloanelor în Latte sau a containerului DI, deoarece aceste biblioteci nu utilizează serviciile nette/caching și își gestionează cache-ul independent. Cache-ul lor, de altfel, nu trebuie dezactivat în modul dezvoltator.