Cache

Cache [keš] zrychlí vaši aplikaci tím, že jednou náročně získaná data uloží pro příští použití. Ukážeme si:

  • jak používat cache
  • jak změnit úložiště
  • jak správně cache invalidovat

Používání cache je v Nette velmi snadné, přitom pokrývá i velmi pokročilé potřeby. Je navrženo pro výkon a 100% odolnost. V základu najdete adaptéry pro nejběžnější backendové úložiště. Umožňuje invalidaci založenou na značkách, časovou expiraci, má ochranu proti cache stampede atd.

Instalace

Knihovnu stáhnete a nainstalujete pomocí nástroje Composer:

composer require nette/caching

Základní použití

Středobodem práce s cache neboli mezipamětí představuje objekt Nette\Caching\Cache. Vytvoříme si jeho instanci a jako parametr předáme konstruktoru tzv. úložiště. Což je objekt reprezentující místo, kam se budou data fyzicky ukládat (databáze, Memcached, soubory na disku, …). K úložišti se dostaneme tak, že si jej necháme předat pomocí dependency injection s typem Nette\Caching\Storage. Vše podstatné se dozvíte v části Úložiště.

Ve verzi 3.0 mělo rozhraní ještě prefix I, takže název byl Nette\Caching\IStorage. A dále konstanty třídy Cache byly psané velkými písmeny, takže třeba Cache::EXPIRE místo Cache::Expire.

Pro následující ukázky předpokládejme, že máme vytvořený alias Cache a v proměnné $storage úložiště.

use Nette\Caching\Cache;

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

Cache je vlastně key–value store, tedy data čteme a zapisujeme pod klíči stejně jako u asociativních polí. Aplikace se skládají z řady nezávislých částí a pokud všechny budou používat jedno úložiště (představte si jeden adresář na disku), dříve nebo později by došlo ke kolizi klíčů. Nette Framework problém řeší tak, že celý prostor rozděluje na jmenné prostory (podadresáře). Každá část programu pak používá svůj prostor s unikátním názvem a k žádné kolizi již dojít nemůže.

Název prostoru uvedeme jako druhý parametr konstruktoru třídy Cache:

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

Nyní můžeme pomocí objektu $cache z mezipaměti číst a zapisovat do ní. K obojímu slouží metoda load(). Prvním argumentem je klíč a druhým PHP callback, který se zavolá, když klíč není nalezen v cache. Callback hodnotu vygeneruje, vrátí a ta se uloží do cache:

$value = $cache->load($key, function () use ($key) {
	$computedValue = /* ... */; // náročný výpočet
	return $computedValue;
});

Pokud druhý parametr neuvedeme $value = $cache->load($key), vrátí se null, pokud položka v cache není.

Prima je, že do cache lze ukládat jakékoliv serializovatelné struktury, nemusí to být jen řetězce. A totéž platí dokonce i pro klíče.

Položku z mezipaměti vymažeme metodou remove():

$cache->remove($key);

Uložit položku do mezipaměti lze také metodou $cache->save($key, $value, array $dependencies = []). Preferovaná je nicméně výše uvedený způsob pomocí load().

Memoizace

Memoizace znamená cachování výsledku volání funkce nebo metody, abyste jej mohli použít příště bez vypočítávání stejné věci znovu a znovu.

Memoizovaně lze volat metody a funkce pomocí call(callable $callback, ...$args):

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

Funkce gethostbyaddr() se tak zavolá pro každý parametr $ip jen jednou a příště už se vrátí hodnota z cache.

Také je možné vytvořit si memoizovaný obal nad metodou nebo funkcí, který lze volat až později:

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

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

$result = $memoizedFactorial(5); // poprvé vypočítá
$result = $memoizedFactorial(5); // podruhé z cache

Expirace & invalidace

S ukládáním do cache je potřeba řešit otázku, kdy se dříve uložená data stanou neplatná. Nette Framework nabízí mechanismus, jak omezit platnost dat nebo je řízeně mazat (v terminologii frameworku „invalidovat“).

Platnost dat se nastavuje v okamžiku ukládání a to pomocí třetího parametru metody save(), např.:

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

Nebo pomocí parametru $dependencies předávaného referencí do callbacku metody load(), např.:

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

Nebo pomocí 3. parametru v metodě load(), např:

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

V dalších ukázkách budeme předpokládat druhou variantu a tedy existenci proměnné $dependencies.

Expirace

Nejjednodušší expirace představuje časový limit. Takto uložíme do cache data s platností 20 minut:

// akceptuje i počet sekund nebo UNIX timestamp
$dependencies[Cache::Expire] = '20 minutes';

Pokud bychom chtěli prodloužit dobu platnosti s každým čtením, lze toho docílit následovně, ale pozor, režie cache tím vzroste:

$dependencies[Cache::Sliding] = true;

Šikovná je možnost nechat data vyexpirovat v okamžiku, kdy se změní soubor či některý z více souborů. Toho lze využít třeba při ukládání dat vzniklých zpracováním těchto souborů do cache. Používejte absolutní cesty.

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

Můžeme nechat položku v cache vyexpirovat ve chvíli, kdy vyexpiruje jiná položka (či některá z více jiných). Což lze využít tehdy, když ukládáme do cache třeba celou HTML stránku a pod jinými klíči její fragmenty. Jakmile se fragment změní, invaliduje se celá stránka. Pokud fragmenty máme uložené pod klíči např. frag1 a frag2, použijeme:

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

Expiraci lze řídit i pomocí vlastních funkcí nebo statických metod, které vždy při čtení rozhodnou, zda je položka ještě platná. Takto třeba můžeme nechat položku vyexpirovat vždy, když se změní verze PHP. Vytvoříme funkci, která porovná aktuální verzi s parameterem, a při ukládání přidáme mezi závislosti pole ve tvaru [nazev funkce, ...argumenty]:

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

$dependencies[Cache::Callbacks] = [
	['checkPhpVersion', PHP_VERSION_ID] // expiruj když checkPhpVersion(...) === false
];

Všechna kritéria je samozřejmě možné kombinovat. Cache pak vyexpiruje, když alespoň jedno kritérium není splněno.

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

Invalidace pomocí tagů

Velmi užitečným invalidačním nástrojem jsou tzv. tagy. Každé položce v cache můžeme přiřadit seznam tagů, což jsou libovolné řetězce. Mějme třeba HTML stránku s článkem a komentáři, kterou budeme cachovat. Při ukládání specifikujeme tagy:

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

Přesuňme se do administrace. Tady najdeme formulář pro editaci článku. Společně s uložením článku do databáze zavoláme příkaz clean(), který smaže z cache položky dle tagu:

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

Stejně tak v místě přidání nového komentáře (nebo editace komentáře) neopomeneme invalidovat příslušný tag:

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

Čeho jsme tím dosáhli? Že se nám HTML cache bude invalidovat (mazat), kdykoliv se změní článek nebo komentáře. Když se edituje článek s ID = 10, dojde k vynucené invalidaci tagu article/10 a HTML stránka, která uvedený tag nese, se z cache smaže. Totéž nastane při vložení nového komentáře pod příslušný článek.

Tagy vyžadují tzv. Journal.

Invalidace pomocí priority

Jednotlivým položkám v cache můžeme nastavit prioritu, pomocí které je bude možné mazat, když třeba cache přesáhne určitou velikost:

$dependencies[Cache::Priority] = 50;

Smažeme všechny položky s prioritou rovnou nebo menší než 100:

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

Priority vyžadují tzv. Journal.

Smazání cache

Parametr Cache::All smaže vše:

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

Hromadné čtení

Pro hromadné čtení a zápisy do cache slouží metoda bulkLoad(), které předáme pole klíčů a získáme pole hodnot:

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

Metoda bulkLoad() funguje podobně jako load() i s druhým parametrem callbackem, kterému se předává klíč generované položky:

$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
	$computedValue = /* ... */; // náročný výpočet
	return $computedValue;
});

Cachování výstupu

Velmi elegantně lze zachytávat a cachovat výstup:

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

	echo ... // vypisujeme data

	$capture->end(); // uložíme výstup do cache
}

V případě, že výstup už je v cache uložen, tak ho metoda capture() vypíše a vrátí null, tedy podmínka se nevykoná. V opačném případě začne výstup zachytávat a vrátí objekt $capture, pomocí něhož nakonec vypsaná data uložíme do cache.

Ve verzi 3.0 se metoda jmenovala $cache->start().

Cachování v Latte

Cachování v šablonách Latte je velmi snadné, stačí část šablony obalit značkami {cache}...{/cache}. Cache se automaticky invaliduje ve chvíli, kdy se změní zdrojová šablona (včetně případných inkludovaných šablon uvnitř bloku cache). Značky {cache} lze vnořovat do sebe a když se vnořený blok zneplatní (například tagem), zneplatní se i blok nadřazený.

Ve značce je možné uvést klíče, na které se bude cache vázat (zde proměnná $id) a nastavit expiraci a tagy pro zneplatnění

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

Všechny položky jsou volitelné, takže nemusíme uvádět ani expiraci, ani tagy, nakonec ani klíče.

Použití cache lze také podmínit pomocí if – obsah se pak bude cachovat pouze bude-li splněna podmínka:

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

Úložiště

Úložiště je objekt reprezentující místo, kam se data fyzicky ukládají. Můžeme použít databázi, server Memcached, nebo nejdostupnější úložiště, což jsou soubory na disku.

Úložiště Popis
FileStorage výchozí úložiště s ukládáním do souborů na disk
MemcachedStorage využívá Memcached server
MemoryStorage data jsou dočasně v paměti
SQLiteStorage data se ukládají do SQLite databáze
DevNullStorage data se neukládají, vhodné pro testování

K objektu úložiště se dostanete tak, že si jej necháte předat pomocí dependency injection s typem Nette\Caching\Storage. Jako výchozí úložiště poskytuje Nette objekt FileStorage ukládající data do podsložky cache v adresáři pro dočasné soubory.

Změnit úložiště můžete v konfiguraci:

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

FileStorage

Zapisuje cache do souborů na disku. Úložiště Nette\Caching\Storages\FileStorage je velmi dobře optimalizované pro výkon a především zajišťuje plnou atomicitu operací. Co to znamená? Že při použití cache se nemůže stát, že přečteme soubor, který ještě není jiným vláknem kompletně zapsaný, nebo že by vám jej někdo „pod rukama“ smazal. Použití cache je tedy zcela bezpečné.

Toto úložiště má také vestavěnou důležitou funkci, která brání před extrémním nárůstem využití CPU ve chvíli, kdy se cache smaže nebo ještě není zahřátá (tj. vytvořená). Jedná se o prevenci před cache stampede. Stává se, že v jednu chvíli se sejde větší počet souběžných požadavků, které chtějí z cache stejnou věc (např. výsledek drahého SQL dotazu) a protože v mezipaměti není, začnou všechny procesy vykonávat stejný SQL dotaz. Vytížení se tak násobí a může se dokonce stát, že žádné vlákno nestihne odpovědět v časovém limitu, cache se nevytvoří a aplikace zkolabuje. Naštěstí cache v Nette funguje tak, že při více souběžných požadavcích na jednu položku ji generuje pouze první vlákno, ostatní čekají a následně využíjí vygenerovaný výsledek.

Příklad vytvoření FileStorage:

// úložištěm bude adresář '/path/to/temp' na disku
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

Server Memcached je vysoce výkonný systém ukládání do distribuované paměti, jehož adaptér je Nette\Caching\Storages\MemcachedStorage. V konfiguraci uvedeme IP adresu a port, pokud se liší od standardního 11211.

Vyžaduje PHP rozšíření memcached.

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

MemoryStorage

Nette\Caching\Storages\MemoryStorage je úložiště, která data ukládá do PHP pole, a tedy se s ukončením požadavku ztratí.

SQLiteStorage

Databáze SQLite a adaptér Nette\Caching\Storages\SQLiteStorage nabízí způsob, jak ukládat cache do jediného souboru na disku. V konfiguraci uvedeme cestu k tomuto souboru.

Vyžaduje PHP rozšíření pdo a pdo_sqlite.

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

DevNullStorage

Speciální implementací úložiště je Nette\Caching\Storages\DevNullStorage, které ve skutečnosti data neukládá vůbec. Je tak vhodné pro testování, když chceme eliminovat vliv cache.

Použití cache v kódu

Při používání cache v kódu máme dva způsoby, jak na to. První z nich je ten, že si necháme předat pomocí dependency injection úložiště a vytvoříme 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');
	}
}

Druhá možnost je, že si necháme rovnou předat objekt Cache:

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

Objekt Cache se potom vytvoří přímo v konfiguraci tímto způsobem:

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

Journal

Nette si tagy a priority ukládá do tzv. journalu. Standardně se k tomu používá SQLite a soubor journal.s3db a vyžadují se PHP rozšíření pdo a pdo_sqlite.

Změnit journal můžete v konfiguraci:

services:
	cache.journal: MyJournal

Služby DI

Tyto služby se přidávají do DI kontejneru:

Název Typ Popis
cache.journal Nette\Caching\Storages\Journal journal
cache.storage Nette\Caching\Storage úložiště

Vypnutí cache

Jednou z možností, jak vypnout cache v aplikaci, je nastavit jako úložiště DevNullStorage:

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

Toto nastavení nemá vliv na kešování šablon v Latte nebo DI kontejeru, protože tyto knihovny nevyužívají služeb nette/caching a spravují si cache samostatně. Jejich cache ostatně není potřeba ve vývojářském režimu vypínat.

verze: 3.x 2.x