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;
});
Použití s PSR-16
Pro použití Nette Cache s rozhraním PSR-16 můžete využít adaptér PsrCacheAdapter
. Umožňuje bezešvou
integraci mezi Nette Cache a jakýmkoli kódem nebo knihovnou, která očekává PSR-16 kompatibilní cache.
$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);
Nyní můžete používat $psrCache
jako PSR-16 cache:
$psrCache->set('key', 'value', 3600); // uloží hodnotu na 1 hodinu
$value = $psrCache->get('key', 'default');
Adaptér podporuje všechny metody definované v PSR-16, včetně getMultiple()
, setMultiple()
, a
deleteMultiple()
.
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.