Cache
Cache [kesz]
przyspieszy twoją aplikację, przechowując raz uzyskane dane do przyszłego użytku. Pokażemy:
- jak używać cache
- jak zmienić magazyn
- jak poprawnie unieważniać cache
Korzystanie z cache w Nette jest bardzo łatwe, a jednocześnie obejmuje bardzo zaawansowane potrzeby. Został zaprojektowany z myślą o wydajności i 100% odporności. W standardzie znajdziesz adaptery do najczęstszych magazynów backendowych. Umożliwia unieważnianie oparte na tagach, wygasanie czasowe, posiada ochronę przed cache stampede itp.
Instalacja
Bibliotekę pobierzesz i zainstalujesz za pomocą narzędzia Composer:
composer require nette/caching
Podstawowe użycie
Centrum pracy z cache, czyli pamięcią podręczną, stanowi obiekt Nette\Caching\Cache. Tworzymy jego instancję i jako
parametr przekazujemy konstruktorowi tzw. magazyn. Jest to obiekt reprezentujący miejsce, gdzie dane będą fizycznie
przechowywane (baza danych, Memcached, pliki na dysku, …). Do magazynu dostaniemy się, prosząc o jego przekazanie za pomocą
dependency injection z typem
Nette\Caching\Storage
. Wszystko, co istotne, dowiesz się w sekcji Magazyny.
W wersji 3.0 interfejs miał jeszcze prefiks I
, więc nazwa brzmiała
Nette\Caching\IStorage
. Ponadto stałe klasy Cache
były pisane wielkimi literami, więc na przykład
Cache::EXPIRE
zamiast Cache::Expire
.
W poniższych przykładach zakładamy, że mamy utworzony alias Cache
i w zmiennej $storage
magazyn.
use Nette\Caching\Cache;
$storage = /* ... */; // instancja Nette\Caching\Storage
Cache jest właściwie key–value store, czyli dane odczytujemy i zapisujemy pod kluczami, tak jak w tablicach asocjacyjnych. Aplikacje składają się z wielu niezależnych części i jeśli wszystkie będą używać jednego magazynu (wyobraź sobie jeden katalog na dysku), wcześniej czy później doszłoby do kolizji kluczy. Nette Framework rozwiązuje ten problem, dzieląc całą przestrzeń na przestrzenie nazw (podkatalogi). Każda część programu używa wtedy swojej przestrzeni z unikalną nazwą i żadna kolizja już nie może wystąpić.
Nazwę przestrzeni podajemy jako drugi parametr konstruktora klasy Cache:
$cache = new Cache($storage, 'Full Html Pages');
Teraz możemy za pomocą obiektu $cache
odczytywać i zapisywać do pamięci podręcznej. Do obu służy metoda
load()
. Pierwszym argumentem jest klucz, a drugim PHP callback, który jest wywoływany, gdy klucz nie zostanie
znaleziony w cache. Callback generuje wartość, zwraca ją, a ta jest zapisywana w cache:
$value = $cache->load($key, function () use ($key) {
$computedValue = /* ... */; // kosztowne obliczenia
return $computedValue;
});
Jeśli drugi parametr nie zostanie podany $value = $cache->load($key)
, zwrócony zostanie null
,
jeśli elementu nie ma w cache.
Świetne jest to, że w cache można przechowywać dowolne serializowalne struktury, nie muszą to być tylko ciągi znaków. To samo dotyczy nawet kluczy.
Element z pamięci podręcznej usuwamy metodą remove()
:
$cache->remove($key);
Zapisać element do pamięci podręcznej można również metodą
$cache->save($key, $value, array $dependencies = [])
. Preferowany jest jednak powyższy sposób za pomocą
load()
.
Memoizacja
Memoizacja oznacza buforowanie wyniku wywołania funkcji lub metody, aby móc go użyć następnym razem bez ponownego obliczania tej samej rzeczy.
Memoizowanie można wywoływać dla metod i funkcji za pomocą call(callable $callback, ...$args)
:
$result = $cache->call('gethostbyaddr', $ip);
Funkcja gethostbyaddr()
zostanie wywołana dla każdego parametru $ip
tylko raz, a następnym razem
zostanie zwrócona wartość z cache.
Możliwe jest również utworzenie memoizowanego opakowania nad metodą lub funkcją, które można wywołać później:
function factorial($num)
{
return /* ... */;
}
$memoizedFactorial = $cache->wrap('factorial');
$result = $memoizedFactorial(5); // oblicza po raz pierwszy
$result = $memoizedFactorial(5); // po raz drugi z cache
Wygaśnięcie & unieważnienie
Przy zapisywaniu do cache trzeba rozwiązać kwestię, kiedy wcześniej zapisane dane stają się nieaktualne. Nette Framework oferuje mechanizm, jak ograniczyć ważność danych lub je kontrolowanie usuwać (w terminologii frameworka „unieważniać“).
Ważność danych ustawia się w momencie zapisywania za pomocą trzeciego parametru metody save()
, np.:
$cache->save($key, $value, [
$cache::Expire => '20 minutes',
]);
Lub za pomocą parametru $dependencies
przekazywanego przez referencję do callbacku metody
load()
, np.:
$value = $cache->load($key, function (&$dependencies) {
$dependencies[Cache::Expire] = '20 minutes';
return /* ... */;
});
Lub za pomocą trzeciego parametru w metodzie load()
, np.:
$value = $cache->load($key, function () {
return ...;
}, [Cache::Expire => '20 minutes']);
W kolejnych przykładach będziemy zakładać drugą opcję, a więc istnienie zmiennej $dependencies
.
Wygaśnięcie
Najprostszym wygaśnięciem jest limit czasowy. W ten sposób zapisujemy dane do cache z ważnością 20 minut:
// akceptuje również liczbę sekund lub timestamp UNIX
$dependencies[Cache::Expire] = '20 minutes';
Jeśli chcielibyśmy przedłużyć okres ważności przy każdym odczycie, można to osiągnąć w następujący sposób, ale uwaga, narzut cache przez to wzrośnie:
$dependencies[Cache::Sliding] = true;
Przydatna jest możliwość wygaśnięcia danych w momencie zmiany pliku lub jednego z wielu plików. Można to wykorzystać na przykład przy zapisywaniu do cache danych powstałych w wyniku przetwarzania tych plików. Używaj ścieżek absolutnych.
$dependencies[Cache::Files] = '/path/to/data.yaml';
// lub
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];
Możemy pozwolić, aby element w cache wygasł w momencie, gdy wygaśnie inny element (lub jeden z wielu innych). Można to
wykorzystać, gdy przechowujemy w cache na przykład całą stronę HTML, a pod innymi kluczami jej fragmenty. Gdy fragment się
zmieni, unieważniona zostanie cała strona. Jeśli fragmenty mamy zapisane pod kluczami np. frag1
i
frag2
, użyjemy:
$dependencies[Cache::Items] = ['frag1', 'frag2'];
Wygaśnięcie można również kontrolować za pomocą własnych funkcji lub metod statycznych, które zawsze przy odczycie
decydują, czy element jest jeszcze ważny. W ten sposób możemy na przykład pozwolić, aby element wygasł zawsze, gdy zmieni
się wersja PHP. Tworzymy funkcję, która porównuje aktualną wersję z parametrem, a przy zapisywaniu dodajemy do zależności
tablicę w formacie [nazwa funkcji, ...argumenty]
:
function checkPhpVersion($ver): bool
{
return $ver === PHP_VERSION_ID;
}
$dependencies[Cache::Callbacks] = [
['checkPhpVersion', PHP_VERSION_ID] // wygaśnij, gdy checkPhpVersion(...) === false
];
Wszystkie kryteria można oczywiście łączyć. Cache wygaśnie wtedy, gdy przynajmniej jedno kryterium nie jest spełnione.
$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';
Unieważnianie za pomocą tagów
Bardzo użytecznym narzędziem do unieważniania są tzw. tagi. Każdemy elementowi w cache możemy przypisać listę tagów, które są dowolnymi ciągami znaków. Załóżmy, że mamy stronę HTML z artykułem i komentarzami, którą będziemy buforować. Przy zapisywaniu określamy tagi:
$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];
Przejdźmy do administracji. Tutaj znajdziemy formularz do edycji artykułu. Wraz z zapisaniem artykułu do bazy danych
wywołamy polecenie clean()
, które usunie z cache elementy według tagu:
$cache->clean([
$cache::Tags => ["article/$articleId"],
]);
Podobnie, w miejscu dodawania nowego komentarza (lub edycji komentarza) nie zapomnijmy unieważnić odpowiedniego tagu:
$cache->clean([
$cache::Tags => ["comments/$articleId"],
]);
Co przez to osiągnęliśmy? Że nasza pamięć podręczna HTML będzie unieważniana (usuwana), gdy tylko zmieni się artykuł
lub komentarze. Kiedy edytowany jest artykuł o ID = 10, następuje wymuszone unieważnienie tagu article/10
, a
strona HTML, która nosi ten tag, zostanie usunięta z cache. To samo nastąpi przy wstawieniu nowego komentarza pod odpowiedni
artykuł.
Tagi wymagają tzw. Journal.
Unieważnianie za pomocą priorytetu
Poszczególnym elementom w cache możemy ustawić priorytet, za pomocą którego będzie można je usuwać, gdy na przykład cache przekroczy określoną wielkość:
$dependencies[Cache::Priority] = 50;
Usuniemy wszystkie elementy o priorytecie równym lub mniejszym niż 100:
$cache->clean([
$cache::Priority => 100,
]);
Priorytety wymagają tzw. Journal.
Czyszczenie cache
Parametr Cache::All
usuwa wszystko:
$cache->clean([
$cache::All => true,
]);
Odczyt masowy
Do masowego odczytu i zapisu do cache służy metoda bulkLoad()
, której przekazujemy tablicę kluczy
i otrzymujemy tablicę wartości:
$values = $cache->bulkLoad($keys);
Metoda bulkLoad()
działa podobnie jak load()
również z drugim parametrem callbackiem, któremu
przekazywany jest klucz generowanego elementu:
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
$computedValue = /* ... */; // kosztowne obliczenia
return $computedValue;
});
Użycie z PSR-16
Aby użyć Nette Cache z interfejsem PSR-16, możesz wykorzystać adapter PsrCacheAdapter
. Umożliwia on
bezproblemową integrację między Nette Cache a dowolnym kodem lub biblioteką, która oczekuje cache zgodnego z PSR-16.
$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);
Teraz możesz używać $psrCache
jako cache PSR-16:
$psrCache->set('key', 'value', 3600); // zapisuje wartość na 1 godzinę
$value = $psrCache->get('key', 'default');
Adapter obsługuje wszystkie metody zdefiniowane w PSR-16, w tym getMultiple()
, setMultiple()
i
deleteMultiple()
.
Buforowanie wyjścia
Bardzo elegancko można przechwytywać i buforować wyjście:
if ($capture = $cache->capture($key)) {
echo ... // wypisujemy dane
$capture->end(); // zapisujemy wyjście do cache
}
W przypadku, gdy wyjście jest już zapisane w cache, metoda capture()
je wypisuje i zwraca null
,
więc warunek się nie wykona. W przeciwnym razie zaczyna przechwytywać wyjście i zwraca obiekt $capture
, za
pomocą którego ostatecznie zapisujemy wypisane dane do cache.
W wersji 3.0 metoda nazywała się $cache->start()
.
Buforowanie w Latte
Buforowanie w szablonach Latte jest bardzo łatwe, wystarczy część szablonu
otoczyć znacznikami {cache}...{/cache}
. Cache jest automatycznie unieważniany w momencie zmiany szablonu
źródłowego (w tym ewentualnych dołączonych szablonów wewnątrz bloku cache). Znaczniki {cache}
można
zagnieżdżać w sobie, a gdy zagnieżdżony blok zostanie unieważniony (na przykład przez tag), unieważniony zostanie
również blok nadrzędny.
W znaczniku można podać klucze, do których będzie powiązany cache (tutaj zmienna $id
) i ustawić
wygaśnięcie oraz tagi do unieważnienia.
{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
...
{/cache}
Wszystkie pozycje są opcjonalne, więc nie musimy podawać ani wygaśnięcia, ani tagów, ani nawet kluczy.
Użycie cache można również uzależnić za pomocą if
– zawartość będzie wtedy buforowana tylko wtedy,
gdy warunek zostanie spełniony:
{cache $id, if: !$form->isSubmitted()}
{$form}
{/cache}
Magazyny
Magazyn to obiekt reprezentujący miejsce, gdzie dane są fizycznie przechowywane. Możemy użyć bazy danych, serwera Memcached lub najłatwiej dostępnego magazynu, jakim są pliki na dysku.
Magazyn | Opis |
---|---|
FileStorage | domyślny magazyn z zapisem do plików na dysku |
MemcachedStorage | wykorzystuje serwer Memcached |
MemoryStorage | dane są tymczasowo w pamięci |
SQLiteStorage | dane są zapisywane do bazy danych SQLite |
DevNullStorage | dane nie są zapisywane, odpowiednie do testowania |
Do obiektu magazynu dostaniesz się, prosząc o jego przekazanie za pomocą dependency injection z typem
Nette\Caching\Storage
. Jako domyślny magazyn Nette dostarcza obiekt FileStorage zapisujący dane do podkatalogu
cache
w katalogu dla plików
tymczasowych.
Zmienić magazyn można w konfiguracji:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
FileStorage
Zapisuje cache do plików na dysku. Magazyn Nette\Caching\Storages\FileStorage
jest bardzo dobrze zoptymalizowany
pod kątem wydajności i przede wszystkim zapewnia pełną atomowość operacji. Co to oznacza? Że podczas używania cache nie
może się zdarzyć, że odczytamy plik, który jeszcze nie został w całości zapisany przez inny wątek, lub że ktoś nam go
“pod ręką” usunie. Użycie cache jest więc całkowicie bezpieczne.
Ten magazyn ma również wbudowaną ważną funkcję, która zapobiega ekstremalnemu wzrostowi zużycia procesora w momencie, gdy cache zostanie usunięty lub jeszcze nie jest rozgrzany (tj. utworzony). Jest to prewencja przed cache stampede. Zdarza się, że w jednym momencie pojawi się większa liczba równoczesnych żądań, które chcą z cache tej samej rzeczy (np. wyniku kosztownego zapytania SQL), a ponieważ w pamięci podręcznej jej nie ma, wszystkie procesy zaczynają wykonywać to samo zapytanie SQL. Obciążenie się w ten sposób mnoży i może nawet dojść do sytuacji, że żaden wątek nie zdąży odpowiedzieć w limicie czasowym, cache się nie utworzy, a aplikacja ulegnie awarii. Na szczęście cache w Nette działa tak, że przy wielu równoczesnych żądaniach o jeden element generuje go tylko pierwszy wątek, pozostałe czekają, a następnie wykorzystują wygenerowany wynik.
Przykład utworzenia FileStorage:
// magazynem będzie katalog '/path/to/temp' na dysku
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');
MemcachedStorage
Serwer Memcached to wysokowydajny system przechowywania w rozproszonej pamięci, którego
adapterem jest Nette\Caching\Storages\MemcachedStorage
. W konfiguracji podajemy adres IP i port, jeśli różni się
od standardowego 11211.
Wymaga rozszerzenia PHP memcached
.
services:
cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')
MemoryStorage
Nette\Caching\Storages\MemoryStorage
to magazyn, który przechowuje dane w tablicy PHP, a więc znikają one wraz
z zakończeniem żądania.
SQLiteStorage
Baza danych SQLite i adapter Nette\Caching\Storages\SQLiteStorage
oferują sposób na przechowywanie cache w
jednym pliku na dysku. W konfiguracji podajemy ścieżkę do tego pliku.
Wymaga rozszerzeń PHP pdo
i pdo_sqlite
.
services:
cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')
DevNullStorage
Specjalną implementacją magazynu jest Nette\Caching\Storages\DevNullStorage
, który w rzeczywistości w ogóle
nie przechowuje danych. Jest więc odpowiedni do testowania, gdy chcemy wyeliminować wpływ cache.
Użycie cache w kodzie
Przy używaniu cache w kodzie mamy dwa sposoby. Pierwszy z nich polega na tym, że prosimy o przekazanie magazynu za pomocą
dependency injection i tworzymy obiekt
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');
}
}
Druga możliwość polega na tym, że prosimy o przekazanie od razu obiektu Cache
:
class ClassTwo
{
public function __construct(
private Nette\Caching\Cache $cache,
) {
}
}
Obiekt Cache
jest następnie tworzony bezpośrednio w konfiguracji w ten sposób:
services:
- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )
Journal
Nette przechowuje tagi i priorytety w tzw. journalu. Standardowo używa się do tego SQLite i pliku journal.s3db
i wymagane są rozszerzenia PHP pdo
i pdo_sqlite
.
Zmienić journal można w konfiguracji:
services:
cache.journal: MyJournal
Usługi DI
Te usługi są dodawane do kontenera DI:
Nazwa | Typ | Opis |
---|---|---|
cache.journal |
Nette\Caching\Storages\Journal | journal |
cache.storage |
Nette\Caching\Storage | magazyn |
Wyłączenie cache
Jedną z możliwości wyłączenia cache w aplikacji jest ustawienie jako magazynu DevNullStorage:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
To ustawienie nie ma wpływu na buforowanie szablonów w Latte ani kontenera DI, ponieważ te biblioteki nie korzystają z usług nette/caching i zarządzają swoją pamięcią podręczną samodzielnie. Ich cache zresztą nie trzeba wyłączać w trybie deweloperskim.