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

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.

versiune: 3.x