Aby optymalizować swoją aplikację przy użyciu cache, należy na początku zadać sobie pytanie, czy taka optymalizacja jest potrzebna oraz czy jest możliwa. Z poprzedniej części artykułu wiemy, że cache jest opłacalny w momencie, gdy mamy do czynienia z powtarzalnymi czynnościami (funkcjami wywoływanymi często z tymi samymi parametrami, w tym samym kontekście). Jeżeli odpowiedź jest twierdząca, trzeba wybrać odpowiedni backend, czyli technologię, która stanowić będzie pamięć cache. Pierwszą myślą mogłoby być użycie systemu plików, lub bazy danych. W istocie – czasami używa się ich jako zaplecze do cache’owania.

Cache „plikowy” i „bazodanowy”

W przypadku wykorzystania systemu plików wpisy cache są tworzone jako pliki o nazwie identyfikującej klucz cache, TTL zaś sprawdzany jest za pomocą daty modyfikacji pliku. W takim rozwiązaniu trudniejsze w implementacji jest rozwiązanie dostarczające mechanizm tagów.
Problemu tego nie ma w przypadku wykorzystywaniu bazy danych. Odpowiednia struktura tabel zapewni nam zarówno utrzymanie informacji n/t tagów, kluczy oraz kontrolę TTL.

Oba te rozwiązania nie są jednak najbardziej optymalne jeśli chodzi o szybkość działania. Odczyt z dysku jest obarczony stosunkowo dużym opóźnieniem, a co za tym idzie nie nadaje się do przechowywania atomowych wpisów cache. Baza danych jest pod tym względem lepsza, natomiast istnieją rozwiązania znacznie lepsze.

Cache w pamięci RAM – Memcache

Jednym z najpopularniejszych rozwiązań backendowych cache jest Memcache. Wykorzystuje on pamięć operacyjną RAM do przechowywania danych. Pamięć ta ma dużo lepsze parametry dostępu niż dysk, czy baza danych. Dzięki temu ten sposób jest najbardziej wydajny jeśli chodzi o prędkość.
Rozwiązanie wykorzystujące Memcache wymaga jednak inwestycji w pamięć RAM. Jeśli  mamy do czynienia z wymagającą aplikacją, która sama w sobie cechuje się dużym zużyciem RAM, wówczas może pojawić się potrzeba, aby Memcache wydzielić na osobny serwer. Tak dzieje się także w praktyce, można także, zgodnie z zasadami skalowalności poziomej, wydzielić kilka serwerów, które będą odpowiedzialne tylko za cache.

Menedżer – warstwa pośrednicząca

Przechodząc do implementacji cache w naszej aplikacji warto zastosować warstwę pośredniczącą pomiędzy logiką biznesową, a samym mechanizmem cache. W tym celu warto wydzielić menedżer cache – obiekt (singleton), który będzie dostarczał aplikacji funkcjonalności cache, zapewniając jednak transparentność zastosowanego backendu.

Przykładowy menedżer mógłby zapewniać następujące funkcje:

[php]
namespace DivanteCache;

class Manager
{

static $instance;

/**
* @return DivanteCacheManager
*/
public static function getInstance()
{
if (!empty(self::$instance)) return self::$instance;

return self::$instance = new self;
}

/**
* @param string $key
* @param mixed $data
* @param int $ttl
* @param array $tags
*/
public function save($key, $data, $ttl, array $tags = array())
{
//…
}

/**
* @param string $key
* @return mixed
*/
public function load($key)
{
if ($this->keyExists($key)) {
//…
}else{
return false;
}
}

/**
* @param string $key
*/
public function clear($key)
{
$this->save($key, false, 0);
}

/**
* @param string $key
* @return bool
*/
protected function keyExists($key)
{
//…
}

}
[/php]

W powyższym kodzie w miejsce tzw. „białych plam” należy zaimplementować logikę odpowiedzialną za obsługę danego (lub danych) backendów cache. Na przykładzie dostarczanego przez powyższy menedżer interfejsu spróbujmy zoptymalizować fragment kodu potęgowania. Pierwotnie kod mógłby wyglądać tak:

[php]
$n = 10;
$nFactorial = 1;
$k = 1;

do{
$nFactorial = $nFactorial * $k;
$k++;
}while($k <= $n);

printf(‚%d! = %d’ . PHP_EOL, $n, $nFactorial);
[/php]

Stosując interfejs zdefiniowanego wcześniej menedżera, zastosowanie cache mogłoby w tym wypadku wyglądać następująco:

[php]
use DivanteCacheManager as CacheManager;
$manager = CacheManager::getInstance();

$n = 10;
$cacheKey = ‚FactorialOf’ . $n;

if (!$nFactorial = $manager->load($cacheKey)) {
$nFactorial = 1;
$k = 1;
do{
$nFactorial = $nFactorial * $k;
$k++;
}while($k <= $n);

$manager->save($cacheKey, $nFactorial, 3600);
}

printf(‚%d! = %d’ . PHP_EOL, $n, $nFactorial);
[/php]

Wraz z nowościami języka PHP możemy sprawić by implementacja cache’owania stała się jeszcze bardziej czytelna. Jest to co prawda tzw. cukier syntaktyczny, niemniej jednak w mojej ocenie bardzo ciekawie wykorzystuje nowe konstrukcje językowe.
Implementacja „nowego” menedżera:

[php]
namespace DivanteCache;

class NewManager extends Manager
{

/**
* @return DivanteCacheManager
*/
public static function getInstance()
{
if (!empty(self::$instance)) return self::$instance;

return self::$instance = new self;
}

/**
* @param string $key
* @param callable $rebuildFunction
* @param int $ttl
* @param array $tags
* @return mixed|void
*/
public function load($key, $rebuildFunction, $ttl, array $tags = array())
{
if ($this->keyExists($key)) {
//…
}else{
$data = call_user_func($rebuildFunction);
$this->save($key, $data, $ttl, $tags);
return $data;
}
}
}
[/php]

Poniżej jego użycie:

[php]
use DivanteCacheNewManager as CacheManager;
$manager = CacheManager::getInstance();

$n = 10;
$cacheKey = ‚FactorialOf’ . $n;

$nFactorial = $manager->load($cacheKey, function() use ($n) {
$nFactorial = 1;
$k = 1;

do{
$nFactorial = $nFactorial * $k;
$k++;
}while($k <= $n);

return $nFactorial;
}, 3600);
[/php]

W następnych częściach artykułu pokażemy, jak z pomocą takiego interfejsu zaimplementować m.in. cache’owanie metod, bloczków, donut caching.