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.