Predpomnilnik

Predpomnilnik [keš] pospeši vašo aplikacijo tako, da enkrat težko pridobljene podatke shrani za naslednjo uporabo. Pokazali bomo:

  • kako uporabljati predpomnilnik
  • kako spremeniti shrambo
  • kako pravilno invalidirati predpomnilnik

Uporaba predpomnilnika je v Nette zelo enostavna, hkrati pa pokriva tudi zelo napredne potrebe. Zasnovan je za zmogljivost in 100% odpornost. V osnovi najdete adapterje za najpogostejše zaledne shrambe. Omogoča invalidacijo, temelječo na značkah, časovni potek, ima zaščito pred cache stampede itd.

Namestitev

Knjižnico prenesete in namestite z orodjem Composer:

composer require nette/caching

Osnovna uporaba

Središče dela s predpomnilnikom predstavlja objekt Nette\Caching\Cache. Ustvarimo si njegovo instanco in kot parameter konstruktorju posredujemo t.i. shrambo. To je objekt, ki predstavlja mesto, kamor se bodo podatki fizično shranjevali (podatkovna baza, Memcached, datoteke na disku, …). Do shrambe pridemo tako, da si jo pustimo posredovati s pomočjo dependency injection s tipom Nette\Caching\Storage. Vse bistveno boste izvedeli v odseku Shrambe.

V različici 3.0 je imel vmesnik še predpono I, zato je bilo ime Nette\Caching\IStorage. Poleg tega so bile konstante razreda Cache zapisane z velikimi črkami, torej na primer Cache::EXPIRE namesto Cache::Expire.

Za naslednje primere predpostavimo, da imamo ustvarjen alias Cache in v spremenljivki $storage shrambo.

use Nette\Caching\Cache;

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

Predpomnilnik je pravzaprav key–value store, torej podatke beremo in zapisujemo pod ključi enako kot pri asociativnih poljih. Aplikacije so sestavljene iz vrste neodvisnih delov in če bi vsi uporabljali eno shrambo (predstavljajte si en imenik na disku), bi prej ali slej prišlo do kolizije ključev. Nette Framework problem rešuje tako, da celoten prostor deli na imenske prostore (podimenike). Vsak del programa nato uporablja svoj prostor z edinstvenim imenom in do nobene kolizije več ne more priti.

Ime prostora navedemo kot drugi parameter konstruktorja razreda Cache:

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

Zdaj lahko s pomočjo objekta $cache iz predpomnilnika beremo in vanj zapisujemo. Za oboje služi metoda load(). Prvi argument je ključ in drugi PHP povratni klic (callback), ki se pokliče, ko ključ ni najden v predpomnilniku. Povratni klic vrednost generira, vrne in ta se shrani v predpomnilnik:

$value = $cache->load($key, function () use ($key) {
	$computedValue = /* ... */; // zahteven izračun
	return $computedValue;
});

Če drugega parametra ne navedemo $value = $cache->load($key), se vrne null, če elementa v predpomnilniku ni.

Odlično je, da lahko v predpomnilnik shranjujemo kakršnekoli serializabilne strukture, ni nujno, da so to samo nizi. In enako velja celo za ključe.

Element iz predpomnilnika izbrišemo z metodo remove():

$cache->remove($key);

Shranjevanje elementa v predpomnilnik je mogoče tudi z metodo $cache->save($key, $value, array $dependencies = []). Vendar je prednostni zgoraj navedeni način s pomočjo load().

Memoizacija

Memoizacija pomeni predpomnjenje rezultata klica funkcije ali metode, da ga lahko uporabite naslednjič brez ponovnega izračunavanja iste stvari.

Memoizirano lahko kličemo metode in funkcije s pomočjo call(callable $callback, ...$args):

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

Funkcija gethostbyaddr() se tako pokliče za vsak parameter $ip samo enkrat in naslednjič se že vrne vrednost iz predpomnilnika.

Prav tako je mogoče ustvariti memoiziran ovoj nad metodo ali funkcijo, ki ga lahko kličemo kasneje:

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

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

$result = $memoizedFactorial(5); // prvič izračuna
$result = $memoizedFactorial(5); // drugič iz predpomnilnika

Potek & invalidacija

Pri shranjevanju v predpomnilnik je treba rešiti vprašanje, kdaj prej shranjeni podatki postanejo neveljavni. Nette Framework ponuja mehanizem, kako omejiti veljavnost podatkov ali jih nadzorovano brisati (v terminologiji ogrodja “invalidirati”).

Veljavnost podatkov se nastavi v trenutku shranjevanja in sicer s pomočjo tretjega parametra metode save(), npr.:

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

Ali s pomočjo parametra $dependencies, posredovanega z referenco v povratni klic metode load(), npr.:

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

Ali s pomočjo 3. parametra v metodi load(), npr.:

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

V nadaljnjih primerih bomo predpostavljali drugo varianto in torej obstoj spremenljivke $dependencies.

Potek

Najenostavnejši potek predstavlja časovna omejitev. Tako shranimo v predpomnilnik podatke z veljavnostjo 20 minut:

// sprejema tudi število sekund ali UNIX časovni žig
$dependencies[Cache::Expire] = '20 minutes';

Če bi želeli podaljšati dobo veljavnosti z vsakim branjem, lahko to dosežemo na naslednji način, vendar pozor, režija predpomnilnika se s tem poveča:

$dependencies[Cache::Sliding] = true;

Priročna je možnost, da podatki potečejo v trenutku, ko se spremeni datoteka ali katera od več datotek. To lahko izkoristimo na primer pri shranjevanju podatkov, nastalih z obdelavo teh datotek, v predpomnilnik. Uporabljajte absolutne poti.

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

Element v predpomnilniku lahko pustimo poteči v trenutku, ko poteče drug element (ali kateri od več drugih). To lahko izkoristimo takrat, ko v predpomnilnik shranjujemo na primer celotno HTML stran in pod drugimi ključi njene fragmente. Takoj ko se fragment spremeni, se invalidira celotna stran. Če imamo fragmente shranjene pod ključi npr. frag1 in frag2, uporabimo:

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

Potek lahko nadzorujemo tudi s pomočjo lastnih funkcij ali statičnih metod, ki vedno ob branju odločijo, ali je element še veljaven. Tako lahko na primer pustimo element poteči vedno, ko se spremeni različica PHP. Ustvarimo funkcijo, ki primerja trenutno različico s parametrom, in pri shranjevanju dodamo med odvisnosti polje v obliki [ime funkcije, ...argumenti]:

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

$dependencies[Cache::Callbacks] = [
	['checkPhpVersion', PHP_VERSION_ID] // poteče, ko checkPhpVersion(...) === false
];

Vsa merila je seveda mogoče kombinirati. Predpomnilnik potem poteče, ko vsaj eno merilo ni izpolnjeno.

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

Invalidacija s pomočjo značk

Zelo uporabno orodje za invalidacijo so t.i. značke. Vsakemu elementu v predpomnilniku lahko ob shranjevanju dodelimo seznam značk, ki so poljubni nizi. Imejmo na primer HTML stran s člankom in komentarji, ki jo bomo predpomnili. Pri shranjevanju specificiramo značke:

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

Premaknimo se v administracijo. Tu najdemo obrazec za urejanje članka. Skupaj s shranjevanjem članka v podatkovno bazo pokličemo ukaz clean(), ki izbriše iz predpomnilnika elemente glede na značko:

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

Enako tako na mestu dodajanja novega komentarja (ali urejanja komentarja) ne pozabimo invalidirati ustrezne značke:

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

Kaj smo s tem dosegli? Da se nam bo HTML predpomnilnik invalidiral (brisal), kadarkoli se spremeni članek ali komentarji. Ko se ureja članek z ID = 10, pride do prisilne invalidacije značke article/10 in HTML stran, ki nosi navedeno značko, se izbriše iz predpomnilnika. Enako se zgodi pri vstavljanju novega komentarja pod ustrezen članek.

Značke zahtevajo t.i. Journal.

Invalidacija s pomočjo prioritete

Posameznim elementom v predpomnilniku lahko nastavimo prioriteto, s pomočjo katere jih bo mogoče brisati, ko na primer predpomnilnik preseže določeno velikost:

$dependencies[Cache::Priority] = 50;

Izbrišemo vse elemente s prioriteto enako ali manjšo od 100:

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

Prioritete zahtevajo t.i. Journal.

Brisanje predpomnilnika

Parameter Cache::All izbriše vse:

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

Množično branje

Za množično branje in pisanje v predpomnilnik služi metoda bulkLoad(), kateri posredujemo polje ključev in dobimo polje vrednosti:

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

Metoda bulkLoad() deluje podobno kot load() tudi z drugim parametrom povratnim klicem, kateremu se posreduje ključ generiranega elementa:

$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
	$computedValue = /* ... */; // zahteven izračun
	return $computedValue;
});

Uporaba s PSR-16

Za uporabo Nette Cache z vmesnikom PSR-16 lahko uporabite adapter PsrCacheAdapter. Omogoča brezšivno integracijo med Nette Cache in katerokoli kodo ali knjižnico, ki pričakuje PSR-16 združljiv predpomnilnik.

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

Zdaj lahko uporabljate $psrCache kot PSR-16 predpomnilnik:

$psrCache->set('key', 'value', 3600); // shrani vrednost za 1 uro
$value = $psrCache->get('key', 'default');

Adapter podpira vse metode, definirane v PSR-16, vključno z getMultiple(), setMultiple() in deleteMultiple().

Predpomnjenje izpisa

Zelo elegantno lahko zajamemo in predpomnimo izpis:

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

	echo ... // izpisujemo podatke

	$capture->end(); // shranimo izpis v predpomnilnik
}

V primeru, da je izpis že shranjen v predpomnilniku, ga metoda capture() izpiše in vrne null, torej se pogoj ne izvede. V nasprotnem primeru začne zajemati izpis in vrne objekt $capture, s pomočjo katerega na koncu izpisane podatke shranimo v predpomnilnik.

V različici 3.0 se je metoda imenovala $cache->start().

Predpomnjenje v Latte

Predpomnjenje v predlogah Latte je zelo enostavno, dovolj je, da del predloge ovijemo z značkami {cache}...{/cache}. Predpomnilnik se samodejno invalidira v trenutku, ko se spremeni izvorna predloga (vključno z morebitnimi vključenimi predlogami znotraj bloka cache). Značke {cache} lahko gnezdijo ena v drugo in ko se vgnezden blok razveljavi (na primer z značko), se razveljavi tudi nadrejeni blok.

V znački je mogoče navesti ključe, na katere bo predpomnilnik vezan (tu spremenljivka $id) in nastaviti potek ter značke za razveljavitev

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

Vsi elementi so neobvezni, zato nam ni treba navajati niti poteka, niti značk, na koncu niti ključev.

Uporabo predpomnilnika lahko tudi pogojimo s pomočjo if – vsebina se bo potem predpomnila samo, če bo pogoj izpolnjen:

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

Shrambe

Shramba je objekt, ki predstavlja mesto, kamor se podatki fizično shranjujejo. Lahko uporabimo podatkovno bazo, strežnik Memcached ali najdostopnejšo shrambo, kar so datoteke na disku.

Shramba Opis
FileStorage privzeta shramba s shranjevanjem v datoteke na disk
MemcachedStorage uporablja Memcached strežnik
MemoryStorage podatki so začasno v pomnilniku
SQLiteStorage podatki se shranjujejo v SQLite podatkovno bazo
DevNullStorage podatki se ne shranjujejo, primerno za testiranje

Do objekta shrambe pridete tako, da si ga pustite posredovati s pomočjo dependency injection s tipom Nette\Caching\Storage. Kot privzeto shrambo Nette ponuja objekt FileStorage, ki shranjuje podatke v podimenik cache v imeniku za začasne datoteke.

Shrambo lahko spremenite v konfiguraciji:

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

FileStorage

Zapisuje predpomnilnik v datoteke na disku. Shramba Nette\Caching\Storages\FileStorage je zelo dobro optimizirana za zmogljivost in predvsem zagotavlja polno atomičnost operacij. Kaj to pomeni? Da se pri uporabi predpomnilnika ne more zgoditi, da bi prebrali datoteko, ki še ni bila popolnoma zapisana s strani druge niti, ali da bi vam jo kdo “pod roko” izbrisal. Uporaba predpomnilnika je torej popolnoma varna.

Ta shramba ima tudi vgrajeno pomembno funkcijo, ki preprečuje ekstremno povečanje uporabe CPU v trenutku, ko se predpomnilnik izbriše ali še ni ogret (tj. ustvarjen). Gre za preprečevanje cache stampede. Zgodi se, da se v enem trenutku zbere večje število sočasnih zahtev, ki želijo iz predpomnilnika isto stvar (npr. rezultat drage SQL poizvedbe) in ker v predpomnilniku ni, začnejo vsi procesi izvajati isto SQL poizvedbo. Obremenitev se tako množi in lahko se celo zgodi, da nobena nit ne uspe odgovoriti v časovni omejitvi, predpomnilnik se ne ustvari in aplikacija propade. Na srečo predpomnilnik v Nette deluje tako, da pri več sočasnih zahtevah za en element ga generira samo prva nit, ostale čakajo in nato uporabijo generirani rezultat.

Primer ustvarjanja FileStorage:

// shramba bo imenik '/path/to/temp' na disku
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

Strežnik Memcached je visoko zmogljiv sistem shranjevanja v porazdeljenem pomnilniku, katerega adapter je Nette\Caching\Storages\MemcachedStorage. V konfiguraciji navedemo IP naslov in vrata, če se razlikujejo od standardnih 11211.

Zahteva PHP razširitev memcached.

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

MemoryStorage

Nette\Caching\Storages\MemoryStorage je shramba, ki podatke shranjuje v PHP polje, in se torej z zaključkom zahteve izgubijo.

SQLiteStorage

Podatkovna baza SQLite in adapter Nette\Caching\Storages\SQLiteStorage ponujata način, kako shranjevati predpomnilnik v eno datoteko na disku. V konfiguraciji navedemo pot do te datoteke.

Zahteva PHP razširitvi pdo in pdo_sqlite.

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

DevNullStorage

Posebna implementacija shrambe je Nette\Caching\Storages\DevNullStorage, ki dejansko podatkov sploh ne shranjuje. Je tako primerna za testiranje, ko želimo eliminirati vpliv predpomnilnika.

Uporaba predpomnilnika v kodi

Pri uporabi predpomnilnika v kodi imamo dva načina, kako to storiti. Prvi je ta, da si pustimo posredovati s pomočjo dependency injection shrambo in ustvarimo objekt 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');
	}
}

Druga možnost je, da si pustimo neposredno posredovati objekt Cache:

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

Objekt Cache se potem ustvari neposredno v konfiguraciji na ta način:

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

Dnevnik (Journal)

Nette si značke in prioritete shranjuje v t.i. dnevnik. Standardno se za to uporablja SQLite in datoteka journal.s3db ter zahtevata se PHP razširitvi pdo in pdo_sqlite.

Dnevnik lahko spremenite v konfiguraciji:

services:
	cache.journal: MyJournal

Storitve DI

Te storitve se dodajo v DI vsebnik:

Ime Tip Opis
cache.journal Nette\Caching\Storages\Journal dnevnik
cache.storage Nette\Caching\Storage shramba

Izklop predpomnilnika

Ena od možnosti, kako izklopiti predpomnilnik v aplikaciji, je nastaviti kot shrambo DevNullStorage:

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

Ta nastavitev nima vpliva na predpomnjenje predlog v Latte ali DI vsebnika, ker te knjižnice ne uporabljajo storitev nette/caching in si upravljajo predpomnilnik samostojno. Njihovega predpomnilnika sicer ni treba v razvojnem načinu izklapljati.

različica: 3.x