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

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.

verzió: 3.x