Кеш

Кешът ускорява вашето приложение, като съхранява данни, които са били трудно получени веднъж, за бъдеща употреба. Ще ви покажем:

  • как да използвате кеша
  • как да промените хранилището
  • как правилно да инвалидирате кеша

Използването на кеша в Nette е много лесно, като същевременно покрива и много напреднали нужди. Той е проектиран за производителност и 100% устойчивост. В основата му ще намерите адаптери за най-често срещаните бекенд хранилища. Позволява инвалидация, базирана на тагове, изтичане на времето, има защита срещу cache stampede и др.

Инсталация

Изтеглете и инсталирайте библиотеката с помощта на Composer:

composer require nette/caching

Основна употреба

Централният елемент на работата с кеша е обектът Nette\Caching\Cache. Създаваме негова инстанция и предаваме на конструктора така нареченото хранилище като параметър. Това е обект, представляващ мястото, където данните ще се съхраняват физически (база данни, Memcached, файлове на диска, …). Достъп до хранилището получаваме, като го поискаме чрез dependency injection с тип Nette\Caching\Storage. Всичко съществено ще научите в раздела Хранилища.

Във версия 3.0 интерфейсът все още имаше префикс I, така че името беше Nette\Caching\IStorage. Освен това константите на класа Cache бяха написани с главни букви, така че например Cache::EXPIRE вместо Cache::Expire.

За следващите примери да предположим, че имаме създаден псевдоним Cache и в променливата $storage – хранилище.

use Nette\Caching\Cache;

$storage = /* ... */; // инстанция на Nette\Caching\Storage

Кешът е всъщност key–value store, тоест четем и записваме данни под ключове, точно както при асоциативните масиви. Приложенията се състоят от редица независими части и ако всички те използват едно хранилище (представете си една директория на диска), рано или късно ще възникне колизия на ключове. Nette Framework решава проблема, като разделя цялото пространство на именни пространства (поддиректории). Всяка част от програмата използва свое пространство с уникално име и вече не може да възникне колизия.

Името на пространството се указва като втори параметър на конструктора на класа Cache:

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

Сега можем да използваме обекта $cache за четене и запис в кеша. За двете цели се използва методът load(). Първият аргумент е ключът, а вторият е PHP callback, който се извиква, когато ключът не е намерен в кеша. Callback генерира стойността, връща я и тя се записва в кеша:

$value = $cache->load($key, function () use ($key) {
	$computedValue = /* ... */; // сложно изчисление
	return $computedValue;
});

Ако вторият параметър не е указан $value = $cache->load($key), ще се върне null, ако елементът не е в кеша.

Страхотно е, че в кеша могат да се съхраняват всякакви сериализуеми структури, не само низове. Същото важи дори и за ключовете.

Изтриваме елемент от кеша с метода remove():

$cache->remove($key);

Записването на елемент в кеша може да се извърши и с метода $cache->save($key, $value, array $dependencies = []). Предпочитаният начин обаче е горепосоченият чрез load().

Мемоизация

Мемоизацията означава кеширане на резултата от извикване на функция или метод, така че да можете да го използвате следващия път, без да изчислявате същото нещо отново и отново.

Методи и функции могат да бъдат извиквани мемоизирано с помощта на call(callable $callback, ...$args):

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

Функцията gethostbyaddr() се извиква само веднъж за всеки параметър $ip, а следващия път стойността се връща от кеша.

Също така е възможно да се създаде мемоизирана обвивка над метод или функция, която може да бъде извикана по-късно:

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

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

$result = $memoizedFactorial(5); // изчислява за първи път
$result = $memoizedFactorial(5); // втори път от кеша

Изтичане & инвалидация

При съхраняването в кеш е необходимо да се реши въпросът кога по-рано съхранените данни стават невалидни. Nette Framework предлага механизъм за ограничаване на валидността на данните или за тяхното контролирано изтриване (в терминологията на framework-а „инвалидиране“).

Валидността на данните се задава в момента на записване чрез третия параметър на метода save(), напр.:

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

Или чрез параметъра $dependencies, предаден по референция към callback-а на метода load(), напр.:

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

Или чрез 3-тия параметър в метода load(), напр:

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

В следващите примери ще предположим втория вариант и следователно съществуването на променливата $dependencies.

Изтичане

Най-простото изтичане е времевият лимит. По този начин съхраняваме данни в кеша с валидност 20 минути:

// приема също брой секунди или UNIX timestamp
$dependencies[Cache::Expire] = '20 minutes';

Ако искаме да удължим срока на валидност при всяко четене, това може да се постигне по следния начин, но внимавайте, режийните разходи на кеша ще се увеличат:

$dependencies[Cache::Sliding] = true;

Удобна е възможността данните да изтекат в момента, в който се промени файл или някой от няколко файла. Това може да се използва например при съхраняване на данни, възникнали при обработката на тези файлове, в кеша. Използвайте абсолютни пътища.

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

Можем да накараме елемент в кеша да изтече в момента, в който изтече друг елемент (или някой от няколко други). Това може да се използва, когато съхраняваме в кеша например цяла HTML страница и под други ключове нейните фрагменти. Щом фрагментът се промени, цялата страница се инвалидира. Ако фрагментите са съхранени под ключове напр. frag1 и frag2, използваме:

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

Изтичането може да се контролира и с помощта на персонализирани функции или статични методи, които при всяко четене решават дали елементът е все още валиден. По този начин например можем да накараме елемент да изтече винаги, когато се промени версията на PHP. Създаваме функция, която сравнява текущата версия с параметъра, и при записване добавяме към зависимостите масив във формат [име на функция, ...аргументи]:

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

$dependencies[Cache::Callbacks] = [
	['checkPhpVersion', PHP_VERSION_ID] // изтече, когато checkPhpVersion(...) === false
];

Всички критерии, разбира се, могат да се комбинират. Кешът тогава изтича, когато поне един критерий не е изпълнен.

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

Инвалидация чрез тагове

Много полезен инструмент за инвалидация са така наречените тагове. Към всеки елемент в кеша можем да присвоим списък с тагове, които са произволни низове. Да вземем например HTML страница със статия и коментари, която ще кешираме. При записване посочваме таговете:

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

Да се преместим в администрацията. Тук намираме форма за редактиране на статия. Заедно със записването на статията в базата данни извикваме командата clean(), която изтрива от кеша елементи според тага:

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

По същия начин, на мястото на добавяне на нов коментар (или редактиране на коментар) не забравяме да инвалидираме съответния таг:

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

Какво постигнахме с това? Че HTML кешът ще се инвалидира (изтрива), когато статията или коментарите се променят. Когато се редактира статия с ID = 10, се извършва принудителна инвалидация на тага article/10 и HTML страницата, която носи посочения таг, се изтрива от кеша. Същото се случва и при вмъкване на нов коментар под съответната статия.

Таговете изискват така наречения Journal.

Инвалидация чрез приоритет

На отделните елементи в кеша можем да зададем приоритет, с който ще може да ги изтриваме, когато например кешът надхвърли определен размер:

$dependencies[Cache::Priority] = 50;

Изтриваме всички елементи с приоритет равен или по-малък от 100:

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

Приоритетите изискват така наречения Journal.

Изтриване на кеша

Параметърът Cache::All изтрива всичко:

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

Групово четене

За групово четене и запис в кеша се използва методът bulkLoad(), на който предаваме масив от ключове и получаваме масив от стойности:

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

Методът bulkLoad() работи подобно на load() и с втория параметър callback, на който се предава ключът на генерирания елемент:

$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
	$computedValue = /* ... */; // сложно изчисление
	return $computedValue;
});

Използване с PSR-16

За използване на Nette Cache с интерфейса PSR-16 можете да използвате адаптера PsrCacheAdapter. Той позволява безпроблемна интеграция между Nette Cache и всеки код или библиотека, която очаква PSR-16 съвместим кеш.

$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);

Сега можете да използвате $psrCache като PSR-16 кеш:

$psrCache->set('key', 'value', 3600); // съхранява стойността за 1 час
$value = $psrCache->get('key', 'default');

Адаптерът поддържа всички методи, дефинирани в PSR-16, включително getMultiple(), setMultiple() и deleteMultiple().

Кеширане на изхода

Много елегантно може да се улавя и кешира изходът:

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

	echo ... // изписваме данни

	$capture->end(); // записваме изхода в кеша
}

В случай, че изходът вече е съхранен в кеша, методът capture() го изписва и връща null, така че условието не се изпълнява. В противен случай започва да улавя изхода и връща обект $capture, с помощта на който накрая записваме изписаните данни в кеша.

Във версия 3.0 методът се наричаше $cache->start().

Кеширане в Latte

Кеширането в шаблоните Latte е много лесно, достатъчно е част от шаблона да се обвие в тагове {cache}...{/cache}. Кешът се инвалидира автоматично в момента, в който се промени изходният шаблон (включително евентуални включени шаблони вътре в кеш блока). Таговете {cache} могат да се влагат един в друг и когато вложен блок се инвалидира (например с таг), се инвалидира и родителският блок.

В тага е възможно да се посочат ключове, към които ще се обвърже кешът (тук променливата $id) и да се зададе изтичане и тагове за инвалидация

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

Всички елементи са незадължителни, така че не е необходимо да посочваме нито изтичане, нито тагове, нито дори ключове.

Използването на кеша може да бъде обусловено и с помощта на if – съдържанието тогава ще се кешира само ако условието е изпълнено:

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

Хранилища

Хранилището е обект, представляващ мястото, където данните се съхраняват физически. Можем да използваме база данни, сървър Memcached или най-достъпното хранилище, което са файлове на диска.

Хранилище Описание
FileStorage хранилище по подразбиране със съхранение във файлове на диска
MemcachedStorage използва Memcached сървър
MemoryStorage данните са временно в паметта
SQLiteStorage данните се съхраняват в SQLite база данни
DevNullStorage данните не се съхраняват, подходящо за тестване

Достъп до обекта на хранилището получавате, като го поискате чрез dependency injection с тип Nette\Caching\Storage. Като хранилище по подразбиране Nette предоставя обект FileStorage, съхраняващ данни в поддиректория cache в директорията за временни файлове.

Можете да промените хранилището в конфигурацията:

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

FileStorage

Записва кеша във файлове на диска. Хранилището Nette\Caching\Storages\FileStorage е много добре оптимизирано за производителност и преди всичко осигурява пълна атомарност на операциите. Какво означава това? Че при използване на кеша не може да се случи да прочетем файл, който все още не е напълно записан от друг поток, или някой да го изтрие “под носа ни”. Използването на кеша е напълно безопасно.

Това хранилище има и вградена важна функция, която предотвратява екстремно нарастване на използването на CPU в момента, когато кешът се изтрие или все още не е загрят (т.е. създаден). Това е превенция срещу cache stampede. Случва се в един момент да се съберат по-голям брой едновременни заявки, които искат от кеша едно и също нещо (например резултат от скъпа SQL заявка) и тъй като то не е в кеша, всички процеси започват да изпълняват същата SQL заявка. Натоварването се умножава и дори може да се случи нито един поток да не успее да отговори в рамките на времевия лимит, кешът да не се създаде и приложението да се срине. За щастие, кешът в Nette работи така, че при повече едновременни заявки за един елемент, той се генерира само от първия поток, останалите чакат и след това използват генерирания резултат.

Пример за създаване на FileStorage:

// хранилището ще бъде директория '/path/to/temp' на диска
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

Сървърът Memcached е високопроизводителна система за съхранение в разпределена памет, чийто адаптер е Nette\Caching\Storages\MemcachedStorage. В конфигурацията посочваме IP адрес и порт, ако се различават от стандартния 11211.

Изисква PHP разширение memcached.

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

MemoryStorage

Nette\Caching\Storages\MemoryStorage е хранилище, което съхранява данни в PHP масив, и следователно те се губят с прекратяването на заявката.

SQLiteStorage

Базата данни SQLite и адаптерът Nette\Caching\Storages\SQLiteStorage предлагат начин за съхраняване на кеша в един файл на диска. В конфигурацията посочваме пътя до този файл.

Изисква PHP разширения pdo и pdo_sqlite.

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

DevNullStorage

Специална имплементация на хранилище е Nette\Caching\Storages\DevNullStorage, което всъщност изобщо не съхранява данни. Подходящо е за тестване, когато искаме да елиминираме влиянието на кеша.

Използване на кеша в кода

При използване на кеша в кода имаме два начина да го направим. Първият е да поискаме хранилището чрез dependency injection и да създадем обект 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');
	}
}

Втората възможност е директно да поискаме обект Cache:

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

Обектът Cache след това се създава директно в конфигурацията по следния начин:

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

Journal

Nette съхранява тагове и приоритети в така наречения journal. Стандартно за това се използва SQLite и файл journal.s3db и се изискват PHP разширения pdo и pdo_sqlite.

Можете да промените journal-а в конфигурацията:

services:
	cache.journal: MyJournal

DI Сървиси

Тези сървиси се добавят към DI контейнера:

Име Тип Описание
cache.journal Nette\Caching\Storages\Journal journal
cache.storage Nette\Caching\Storage хранилище

Изключване на кеша

Една от възможностите за изключване на кеша в приложението е да се зададе като хранилище DevNullStorage:

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

Тази настройка не влияе на кеширането на шаблони в Latte или DI контейнера, тъй като тези библиотеки не използват услугите на nette/caching и управляват кеша си самостоятелно. Техният кеш впрочем не е необходимо да се изключва в режим на разработка.

версия: 3.x