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
Nette Framework nabízí velmi intuitivní API pro práci s cache. Ostatně, nic jiného jste asi ani nečekali, že? ;-) Než si ukážeme první příklad, je třeba zvážit, kam se mají data fyzicky ukládat. Můžeme použít databázi, server Memcached, nebo nejdostupnější úložiště, což je pevný disk:
// úložištěm bude adresář 'temp' na disku
$storage = new Nette\Caching\Storages\FileStorage('temp');
Ú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é.
Pro samotnou práci s cache používáme objekt Nette\Caching\Cache:
use Nette\Caching\Cache;
$cache = new Cache($storage);
Obsah proměnné $data zapíšeme do cache pod klíčem
$key:
$cache->save($key, $data);
A takto z cache čteme: (pokud položka v cache není, vrátí metoda NULL)
$value = $cache->load($key);
if ($value === NULL) ...
Položku proto z cache vymažeme uložením NULL nebo funkcí remove:
$cache->save($key, NULL);
// or
$cache->remove($key);
Do cache lze ukládat jakékoliv struktury, nemusí to být jen řetězec. A totéž platí i pro klíče.
Webové aplikace se běžně skládají z celé řady vzájemně
nezávislých částí, a pokud všechny cachují data do společného
úložiště (tedy třeba adresáře), dříve nebo později by došlo ke kolizi
názvů. Nette Framework problém řeší tak, že celý prostor rozděluje na
sekce (v případě FileStorage jde o podadresáře). Každá
část programu pak používá svou sekci s unikátním názvem a k žádné
kolizi již dojít nemůže. Název sekce uvedeme jako druhý parametr
konstruktoru třídy Cache:
$cache = new Cache($storage, 'HtmlOutput');
Služba cacheStorage
Abychom si nemuseli všude vytvářet nebo předávat objekt
$storage, lze využít systémový DI kontejner, který disponuje
službou cacheStorage s výchozím úložištěm ukládajícím
data do adresáře určeného parametrem tempDir resp. konstantou
TEMP_DIR.
// $container je systémový kontejner
$cache = new Cache($container->cacheStorage, 'htmlFront');
$cache->save($key, $data);
Cachování v šablonách
Cachování v šablonách 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ý.
V makru 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}
Pokud teprve v šabloně získáváme data z modelu (princip on demand nebo-li lazy), bude cache obzvlášť efektivní.
Cachování výsledku funkce
Cachování výsledku volání funkce nebo metody lze zapsat pomocí metody
call():
$name = $cache->call('gethostbyaddr', $ip);
Funkce gethostbyaddr($ip) se tak zavolá jen jednou a příště
už se vrací hodnota z cache. Samozřejmě pro různé $ip se
cachují různé výsledky.
Obdobně je možné obalit si nějakou operaci cachí a volat až později.
function calculate ($number) {
return 'number is ' . $number;
}
$wrapper = $cache->wrap('calculate');
$result = $wrapper(1); // number is 1
$result = $wrapper(2); // number is 2
Cachování výstupu
Výstup můžeme cachovat nejen v šablonách:
if ($block = $cache->start($key)) [
... vypisujeme data ..
$block->end(); // uložíme výstup do cache
}
V případě, že výstup už je v cache uložen, tak ho metoda
start() vypíše a vrátí NULL. V opačném
případě začne výstup odchytávat a vrátí objekt $block,
pomocí něhož nakonec vypsaná data uložíme do cache.
Expirace a invalidace
S ukládáním dat do cache vznikají dva problémy. Jednak je tu pochopitelně hrozba, že se úložiště zcela zaplní a nebude možné další data zapisovat. A také se může stát, že některá dříve uložená data se stanou v průběhu času neplatná. Nette Framework proto 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:
$cache->save($key, $data, array(
Cache::EXPIRE => '+ 20 minutes', // akceptuje i sekundy nebo timestamp
));
Z kódu je patrné, že jsme data uložili s platností 20 minut. Po
uplynutí této doby bude cache hlásit, že pod klíčem $key
žádný záznam nemá (tj. vrátí NULL). Pokud bychom chtěli
prodloužit dobu platnosti s každým čtením, lze toho docílit takto:
$cache->save($key, $data, array(
Cache::EXPIRE => '+ 20 minutes',
Cache::SLIDING => TRUE,
));
Šikovná je možnost nechat data vyexpirovat v okamžiku, kdy se změní určitý soubor či některý z více souborů. Toho lze využít třeba při ukládání dat vzniklých parsováním těchto souborů do cache. Pro bezproblémovou funkčnost je třeba používat absolutní cesty.
$cache->save($key, $data, array(
Cache::FILES => 'data.yaml', // lze uvést i pole souborů
));
Kritérium Cache::FILES je samozřejmě možné kombinovat
i s časovou expirací Cache::EXPIRE apod.
Cache může být závislá i na jiných položkách cache. 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 část změní, invaliduje se celá stránka.
$cache->save('page', $html, array(
// vyexpiruje, když vyexpiruje frag1 nebo frag2
Cache::ITEMS => array('frag1', 'frag2'),
));
Expiraci lze řídit i pomocí vlastních callbacků:
function controlExpiration($val)
{
return $val;
}
$cache->save($key, $value, array(
Cache::CALLBACKS => array(array('controlExpiration', 1)),
));
Expirace pomocí tagů a priority
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ů. Mějme třeba HTML stránku s článkem a komentáři, kterou budeme cachovat. Při ukládání specifikujeme tagy:
$cache->save($articleId, $html, array(
Cache::TAGS => array("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(array(
Cache::TAGS => array("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(array(
Cache::TAGS => array("comments/$articleId"),
));
Čeho jsme tím dosáhli? Že se nám HTML cache bude invalidovat (mazat)
sama. Kdykoliv uživatel změní č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.
Obdobou tagů je řízení expirace dle priority:
$cache->save($key, $value, array(
Cache::PRIORITY => 50,
));
// smažeme všechny položky s prioritou rovnou nebo menší než 100:
$cache->clean(array(
Cache::PRIORITY => 100,
));
Úložiště
Kromě představeného úložiště FileStorage najdete v Nette
Framework také MemcachedStorage
ukládající data na server Memcached a MemoryStorage
pro ukládání do paměti s životností aktuálního požadavku.
Vypnutí cache pro testování
Speciální úložiště DevNullStorage, které ve skutečnosti nic nedělá, lze použít pro testování, kdy chceme eliminovat vliv cache.
Nastavíme v config.neon:
services:
templateCacheStorage:
class: Nette\Caching\Storages\DevNullStorage
cacheStorage:
class: Nette\Caching\Storages\DevNullStorage
Lze si také napsat vlastní úložiště, jediný požadavek je, aby implementovalo rozhraní IStorage.
@serializationVersion
Do cache můžeme ukládat i objekty. Ale co když se podoba třídy změní
a někde v cache leží její stará serializovaná podoba? To by mohlo při
čtení vytvořit vadný objekt. Co s tím? Stačí u třídy uvést anotaci
@serializationVersion obsahující jakousi verzi třídy:
/**
* @serializationVersion 123
*/
class Data
{
...
}
Při uložení objektu do cache si Nette Framework zapamatuje hodnotu této anotace. Když se poté datová podoba třídy změní, stačí změnit číslo verze v anotaci a framework bude staré objekty automaticky z cache mazat.
Konkurenční cachování
Smazání cache je běžná operace po nahrání nové verze aplikace na server. V tu chvíli ovšem dostane server docela zabrat, protože musí vybudovat cache novou. Získání některých dat může být dost náročné, příkladem je třeba cache pro RobotLoader. A pokud navíc v krátkém okamžiku přijde třeba 30 požadavků a všechny se snaží o totéž, náročnost ještě násobně roste.
Řešením je upravit chování aplikace tak, aby data vytvářelo vždy jen jedno vlákno a ostatní čekala. K tomu stačí jediné – uvést hodnotu ve formě callbacku nebo anonymní funkce:
$result = $cache->save($key, function() { // nebo callback(...)
return buildData(); // náročná operace
});
Framework pak zajistí, že tělo funkce bude v jednu chvíli volat jen jedno vlákno a ostatní budou čekat. Pokud vlákno z nějakého důvodu selže, dostane šanci jiné.
Komentáře 
gawan | 23. 8. 2011, 12:13 | question
expire => '+20 minutes', tu sa dá použiť všetko to vo
funkcii strtotime ? Ak
áno, mohlo by sa to pridať aj do dokumentácie.
carera | 19. 2. 2012, 18:06 | comment
Jak aktuální je tato dokumentace? $cache->remove($key) už neexistuje.
Jan Tvrdík | 19. 2. 2012, 19:25 | comment
Proč si to myslíš? Já ji v API pořád vidím.
carera | 20. 2. 2012, 15:21 | comment
Děkuji, omlouvám se, stydím se. Mám starší verzi Nette :)

Josef Kašpar | 28. 6. 2011, 23:41 | comment
Jak vypnout cache (nahrazení
DevNullStorage): http://forum.nette.org/…-v-nette-2-0