Cache
A Cache felgyorsítja az alkalmazást azáltal, hogy az egyszer nehezen megszerzett adatokat elmenti a későbbi felhasználásra. Megmutatjuk:
- hogyan használjuk a cache-t
- hogyan változtassuk meg a tárolót
- hogyan érvénytelenítsük helyesen a cache-t
A cache használata a Nette-ben nagyon egyszerű, miközben nagyon fejlett igényeket is lefed. Teljesítményre és 100%-os ellenállóságra tervezték. Alapból adaptereket talál a leggyakoribb háttértárolókhoz. Lehetővé teszi a tag-ek alapján történő érvénytelenítést, az időbeli lejárást, védelmet nyújt a cache stampede ellen stb.
Telepítés
A könyvtárat a Composer eszközzel töltheti le és telepítheti:
composer require nette/caching
Alapvető használat
A cache-sel vagy gyorsítótárral való munka középpontjában az Nette\Caching\Cache objektum áll. Létrehozunk egy
példányt belőle, és paraméterként átadjuk a konstruktornak az úgynevezett tárolót. Ez egy olyan objektum, amely azt a
helyet képviseli, ahol az adatok fizikailag tárolódnak (adatbázis, Memcached, fájlok a lemezen, …). A tárolóhoz úgy
juthatunk hozzá, hogy dependency injection
segítségével kérjük át a Nette\Caching\Storage
típussal. Minden lényegeset megtudhat a Tárolók szakaszban.
A 3.0-s verzióban az interfésznek még volt I
előtagja, tehát a neve
Nette\Caching\IStorage
volt. Továbbá a Cache
osztály konstansai nagybetűkkel voltak írva, tehát
például Cache::EXPIRE
a Cache::Expire
helyett.
A következő példákhoz feltételezzük, hogy létrehoztunk egy Cache
aliast, és a $storage
változóban van a tároló.
use Nette\Caching\Cache;
$storage = /* ... */; // instance of Nette\Caching\Storage
A cache valójában egy key–value store, tehát az adatokat kulcsok alatt olvassuk és írjuk, ugyanúgy, mint az asszociatív tömböknél. Az alkalmazások számos független részből állnak, és ha mindegyik ugyanazt a tárolót használná (képzeljünk el egyetlen könyvtárat a lemezen), előbb-utóbb kulcsütközés következne be. A Nette Framework ezt a problémát úgy oldja meg, hogy az egész teret névtérekre (alkönyvtárakra) osztja. Minden programrész ezután a saját, egyedi nevű terét használja, és így már nem fordulhat elő ütközés.
A névtér nevét a Cache osztály konstruktorának második paramétereként adjuk meg:
$cache = new Cache($storage, 'Full Html Pages');
Most már a $cache
objektum segítségével olvashatunk a gyorsítótárból és írhatunk bele. Mindkettőre a
load()
metódus szolgál. Az első argumentum a kulcs, a második pedig egy PHP callback, amely akkor hívódik meg,
ha a kulcs nem található a cache-ben. A callback generálja az értéket, visszaadja, és az elmentődik a cache-be:
$value = $cache->load($key, function () use ($key) {
$computedValue = /* ... */; // költséges számítás
return $computedValue;
});
Ha a második paramétert nem adjuk meg $value = $cache->load($key)
, akkor null
-t ad vissza, ha az
elem nincs a cache-ben.
Nagyszerű, hogy a cache-be bármilyen szerializálható struktúrát tárolhatunk, nem csak stringeket. És ugyanez igaz még a kulcsokra is.
Az elemet a gyorsítótárból a remove()
metódussal töröljük:
$cache->remove($key);
Elemet a gyorsítótárba a $cache->save($key, $value, array $dependencies = [])
metódussal is menthetünk.
Azonban a fentebb bemutatott load()
használata preferált.
Memoizáció
A memoizáció egy függvény vagy metódus hívásának eredményének gyorsítótárazását jelenti, hogy legközelebb újra felhasználhassuk anélkül, hogy újra kiszámítanánk ugyanazt.
Metódusokat és függvényeket memoizáltan hívhatunk a call(callable $callback, ...$args)
segítségével:
$result = $cache->call('gethostbyaddr', $ip);
A gethostbyaddr()
függvény így minden $ip
paraméterre csak egyszer hívódik meg, és
legközelebb már a cache-ből adódik vissza az érték.
Lehetőség van arra is, hogy egy memoizált burkolót hozzunk létre egy metódus vagy függvény köré, amelyet később hívhatunk meg:
function factorial($num)
{
return /* ... */;
}
$memoizedFactorial = $cache->wrap('factorial');
$result = $memoizedFactorial(5); // először kiszámítja
$result = $memoizedFactorial(5); // másodszor a cache-ből
Lejárat & érvénytelenítés
A cache-be való mentéskor felmerül a kérdés, hogy a korábban elmentett adatok mikor válnak érvénytelenné. A Nette Framework egy mechanizmust kínál az adatok érvényességének korlátozására vagy azok irányított törlésére (a keretrendszer terminológiájában „érvénytelenítésére”).
Az adatok érvényességét a mentéskor állítjuk be a save()
metódus harmadik paraméterével, pl.:
$cache->save($key, $value, [
$cache::Expire => '20 minutes',
]);
Vagy a load()
metódus callbackjének referenciaként átadott $dependencies
paraméterével, pl.:
$value = $cache->load($key, function (&$dependencies) {
$dependencies[Cache::Expire] = '20 minutes';
return /* ... */;
});
Vagy a load()
metódus 3. paraméterével, pl.:
$value = $cache->load($key, function () {
return ...;
}, [Cache::Expire => '20 minutes']);
A további példákban a második változatot feltételezzük, és így a $dependencies
változó
létezését.
Lejárat
A legegyszerűbb lejárat az időkorlát. Így 20 perces érvényességgel mentünk adatokat a cache-be:
// elfogadja a másodpercek számát vagy UNIX timestamp-et is
$dependencies[Cache::Expire] = '20 minutes';
Ha minden olvasással meg szeretnénk hosszabbítani az érvényességi időt, azt a következőképpen érhetjük el, de vigyázat, a cache rezsije ezzel megnő:
$dependencies[Cache::Sliding] = true;
Ügyes lehetőség, hogy az adatokat akkor járassuk le, amikor egy fájl vagy több fájl közül valamelyik megváltozik. Ezt például akkor használhatjuk, ha ezeknek a fájloknak a feldolgozásából származó adatokat mentjük a cache-be. Használjon abszolút elérési utakat.
$dependencies[Cache::Files] = '/path/to/data.yaml';
// vagy
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];
Lejárathatunk egy elemet a cache-ben akkor, amikor egy másik elem (vagy több másik közül valamelyik) lejár. Ezt akkor
használhatjuk, ha például egy egész HTML oldalt mentünk a cache-be, és más kulcsok alatt annak töredékeit. Amint egy
töredék megváltozik, az egész oldal érvénytelenné válik. Ha a töredékeket pl. frag1
és frag2
kulcsok alatt tároljuk, használjuk ezt:
$dependencies[Cache::Items] = ['frag1', 'frag2'];
A lejáratot saját függvényekkel vagy statikus metódusokkal is vezérelhetjük, amelyek minden olvasáskor eldöntik, hogy
az elem még érvényes-e. Így például lejárathatunk egy elemet mindig, amikor a PHP verziója megváltozik. Létrehozunk egy
függvényt, amely összehasonlítja az aktuális verziót a paraméterrel, és a mentéskor hozzáadjuk a függőségek közé a
[függvény neve, ...argumentumok]
formátumú tömböt:
function checkPhpVersion($ver): bool
{
return $ver === PHP_VERSION_ID;
}
$dependencies[Cache::Callbacks] = [
['checkPhpVersion', PHP_VERSION_ID] // járjon le, ha checkPhpVersion(...) === false
];
Természetesen minden kritérium kombinálható. A cache akkor jár le, ha legalább egy kritérium nem teljesül.
$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';
Érvénytelenítés tag-ekkel
Nagyon hasznos érvénytelenítő eszközök az úgynevezett tag-ek. Minden cache-beli elemhez hozzárendelhetünk egy tag-listát, amelyek tetszőleges stringek. Legyen például egy HTML oldalunk egy cikkel és hozzászólásokkal, amelyet gyorsítótárazni fogunk. Mentéskor megadjuk a tag-eket:
$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];
Lépjünk át az adminisztrációba. Itt találunk egy űrlapot a cikk szerkesztéséhez. A cikk adatbázisba mentésével
együtt meghívjuk a clean()
parancsot, amely törli a cache-ből az elemeket a tag alapján:
$cache->clean([
$cache::Tags => ["article/$articleId"],
]);
Ugyanígy az új hozzászólás hozzáadásának (vagy egy hozzászólás szerkesztésének) helyén ne felejtsük el érvényteleníteni a megfelelő tag-et:
$cache->clean([
$cache::Tags => ["comments/$articleId"],
]);
Mit értünk el ezzel? Azt, hogy a HTML cache érvénytelenné válik (törlődik), amikor a cikk vagy a hozzászólások
megváltoznak. Ha egy 10-es ID-jú cikket szerkesztünk, akkor kényszerített érvénytelenítés történik az
article/10
tag-re, és a HTML oldal, amely ezt a tag-et hordozza, törlődik a cache-ből. Ugyanez történik egy új
hozzászólás beszúrásakor a megfelelő cikk alá.
A tag-ekhez úgynevezett Journal szükséges.
Érvénytelenítés prioritással
Az egyes cache-elemekhez beállíthatunk prioritást, amellyel törölhetjük őket, ha például a cache meghalad egy bizonyos méretet:
$dependencies[Cache::Priority] = 50;
Töröljük az összes elemet, amelyek prioritása 100 vagy annál kisebb:
$cache->clean([
$cache::Priority => 100,
]);
A prioritásokhoz úgynevezett Journal szükséges.
Cache törlése
A Cache::All
paraméter mindent töröl:
$cache->clean([
$cache::All => true,
]);
Tömeges olvasás
A cache-ből való tömeges olvasásra és írásra a bulkLoad()
metódus szolgál, amelynek átadunk egy
kulcstömböt, és egy értéktömböt kapunk vissza:
$values = $cache->bulkLoad($keys);
A bulkLoad()
metódus hasonlóan működik, mint a load()
, a második paraméter callbackkel is,
amelynek átadódik a generált elem kulcsa:
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
$computedValue = /* ... */; // költséges számítás
return $computedValue;
});
Használat PSR-16-tal
A Nette Cache PSR-16 interfésszel való használatához használhatja a PsrCacheAdapter
adaptert. Lehetővé
teszi a zökkenőmentes integrációt a Nette Cache és bármely olyan kód vagy könyvtár között, amely PSR-16 kompatibilis
cache-t vár.
$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);
Most már használhatja a $psrCache
-t PSR-16 cache-ként:
$psrCache->set('key', 'value', 3600); // 1 órára menti az értéket
$value = $psrCache->get('key', 'default');
Az adapter támogatja az összes PSR-16-ban definiált metódust, beleértve a getMultiple()
,
setMultiple()
és deleteMultiple()
metódusokat is.
Kimenet gyorsítótárazása
Nagyon elegánsan lehet a kimenetet elfogni és gyorsítótárazni:
if ($capture = $cache->capture($key)) {
echo ... // kiírjuk az adatokat
$capture->end(); // elmentjük a kimenetet a cache-be
}
Abban az esetben, ha a kimenet már a cache-ben van, a capture()
metódus kiírja azt és null
-t ad
vissza, tehát a feltétel nem teljesül. Ellenkező esetben elkezdi a kimenet elfogását és visszaadja a $capture
objektumot, amelynek segítségével végül elmentjük a kiírt adatokat a cache-be.
A 3.0-s verzióban a metódus neve $cache->start()
volt.
Gyorsítótárazás Latte-ban
A sablonokban való gyorsítótárazás a Latte-ban nagyon egyszerű, csak a sablon
egy részét kell {cache}...{/cache}
tagekkel körbevenni. A cache automatikusan érvénytelenné válik, amikor a
forrás sablon megváltozik (beleértve az esetlegesen beillesztett sablonokat a cache blokkon belül). A {cache}
tagek egymásba ágyazhatók, és ha egy beágyazott blokk érvénytelenné válik (például egy tag miatt), akkor a
fölérendelt blokk is érvénytelenné válik.
A tagben megadhatók kulcsok, amelyekhez a cache kötődni fog (itt a $id
változó), és beállítható a
lejárat és a címkék az érvénytelenítéshez.
{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
...
{/cache}
Minden elem opcionális, így nem kell megadnunk sem a lejáratot, sem a címkéket, végül még a kulcsokat sem.
A cache használata feltételhez is köthető az if
segítségével – a tartalom csak akkor lesz
gyorsítótárazva, ha a feltétel teljesül:
{cache $id, if: !$form->isSubmitted()}
{$form}
{/cache}
Tárolók
A tároló egy objektum, amely azt a helyet képviseli, ahol az adatok fizikailag tárolódnak. Használhatunk adatbázist, Memcached szervert, vagy a leginkább elérhető tárolót, ami a lemezen lévő fájlok.
Tároló | Leírás |
---|---|
FileStorage | alapértelmezett tároló, amely a lemezen lévő fájlokba ment |
MemcachedStorage | Memcached szervert használ |
MemoryStorage | az adatok ideiglenesen a memóriában vannak |
SQLiteStorage | az adatok SQLite adatbázisba mentődnek |
DevNullStorage | az adatok nem mentődnek, tesztelésre alkalmas |
A tároló objektumhoz úgy juthat hozzá, hogy dependency injection segítségével kéri át a
Nette\Caching\Storage
típussal. Alapértelmezett tárolóként a Nette a FileStorage objektumot biztosítja, amely
az adatokat az ideiglenes fájlok
könyvtárában lévő cache
alkönyvtárba menti.
A tárolót a konfigurációban módosíthatja:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
FileStorage
A cache-t fájlokba írja a lemezen. A Nette\Caching\Storages\FileStorage
tároló nagyon jól optimalizált a
teljesítményre, és mindenekelőtt biztosítja a műveletek teljes atomicitását. Mit jelent ez? Azt, hogy a cache
használatakor nem fordulhat elő, hogy olyan fájlt olvassunk be, amelyet egy másik szál még nem írt ki teljesen, vagy hogy
valaki “a kezünk alól” törölje azt. A cache használata tehát teljesen biztonságos.
Ez a tároló egy fontos beépített funkcióval is rendelkezik, amely megakadályozza a CPU extrém kihasználtságának növekedését abban a pillanatban, amikor a cache törlődik vagy még nincs felmelegítve (azaz létrehozva). Ez a cache stampede elleni védelem. Előfordul, hogy egy időben több párhuzamos kérés érkezik, amelyek ugyanazt a dolgot akarják a cache-ből (pl. egy drága SQL lekérdezés eredményét), és mivel az nincs a gyorsítótárban, minden folyamat ugyanazt az SQL lekérdezést kezdi el végrehajtani. A terhelés így megsokszorozódik, és akár az is előfordulhat, hogy egyetlen szál sem tud válaszolni az időkorláton belül, a cache nem jön létre, és az alkalmazás összeomlik. Szerencsére a Nette cache úgy működik, hogy több párhuzamos kérés esetén egy elemre csak az első szál generálja azt, a többiek várnak, majd felhasználják a generált eredményt.
Példa a FileStorage létrehozására:
// a tároló a '/path/to/temp' könyvtár lesz a lemezen
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');
MemcachedStorage
A Memcached szerver egy nagy teljesítményű, elosztott memóriában történő
tárolási rendszer, amelynek adaptere a Nette\Caching\Storages\MemcachedStorage
. A konfigurációban megadjuk az
IP-címet és a portot, ha az eltér a standard 11211-től.
Szükséges a memcached
PHP kiterjesztés.
services:
cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')
MemoryStorage
A Nette\Caching\Storages\MemoryStorage
egy olyan tároló, amely az adatokat egy PHP tömbben tárolja, és így a
kérés befejeztével elvesznek.
SQLiteStorage
Az SQLite adatbázis és az Nette\Caching\Storages\SQLiteStorage
adapter lehetőséget kínál a cache egyetlen
fájlba történő mentésére a lemezen. A konfigurációban megadjuk ennek a fájlnak az elérési útját.
Szükséges a pdo
és pdo_sqlite
PHP kiterjesztés.
services:
cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')
DevNullStorage
A tároló speciális implementációja a Nette\Caching\Storages\DevNullStorage
, amely valójában egyáltalán
nem tárol adatokat. Így tesztelésre alkalmas, amikor ki akarjuk küszöbölni a cache hatását.
Cache használata a kódban
A cache kódban való használatakor kétféleképpen járhatunk el. Az első az, hogy dependency injection segítségével átkérjük a
tárolót, és létrehozunk egy Cache
objektumot:
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 második lehetőség az, hogy közvetlenül a Cache
objektumot kérjük át:
class ClassTwo
{
public function __construct(
private Nette\Caching\Cache $cache,
) {
}
}
A Cache
objektumot ezután közvetlenül a konfigurációban hozzuk létre ezzel a módszerrel:
services:
- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )
Journal
A Nette a címkéket és prioritásokat az úgynevezett journalba menti. Alapértelmezés szerint ehhez SQLite-ot és a
journal.s3db
fájlt használja, és szükséges a pdo
és pdo_sqlite
PHP
kiterjesztés.
A journalt a konfigurációban módosíthatja:
services:
cache.journal: MyJournal
DI szolgáltatások
Ezek a szolgáltatások kerülnek hozzáadásra a DI konténerhez:
Név | Típus | Leírás |
---|---|---|
cache.journal |
Nette\Caching\Storages\Journal | journal |
cache.storage |
Nette\Caching\Storage | tároló |
Cache kikapcsolása
Az alkalmazásban a cache kikapcsolásának egyik módja, ha a DevNullStorage-t állítjuk be tárolóként:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
Ez a beállítás nincs hatással a sablonok gyorsítótárazására a Latte-ban vagy a DI konténerben, mivel ezek a könyvtárak nem használják a nette/caching szolgáltatásait, és önállóan kezelik a cache-t. Egyébként a cache-üket nem szükséges kikapcsolni fejlesztői módban.