Кэширование

Кэш ускоряет работу вашего приложения, сохраняя данные — однажды с трудом извлеченные — для использования в будущем. Мы покажем вам:

  • Как использовать кэш
  • Как изменить хранилище кэша
  • Как правильно аннулировать кэш

Использование кэша в Nette очень простое, при этом он также покрывает очень сложные потребности в кэшировании. Он разработан для обеспечения производительности и 100% долговечности. В основном, вы найдете адаптеры для наиболее распространенных внутренних хранилищ. Позволяет аннулирование на основе тегов, защиту кэш-памяти, истечение времени и т. д.

Установка

Загрузите и установите пакет с помощью Composer:

composer require nette/caching

Использование

Центром работы с кэшем является объект Nette\Caching\Cache. Мы создаем его экземпляр и передаем конструктору в качестве параметра так называемое хранилище. Это объект, представляющий место, где данные будут физически храниться (база данных, Memcached, файлы на диске, …). Вы получаете объект хранилища, передавая его с помощью внедрения зависимостей с типом Nette\Caching\Storage. Всё самое необходимое вы найдете в разделе Хранилища.

Для следующих примеров предположим, что у нас есть псевдоним Cache и хранилище в переменной $storage.

use Nette\Caching\Cache;

$storage = /* ... */; // экземпляр Nette\Caching\Storage

Кэш фактически является хранилищем типа ключ-значение, поэтому мы читаем и записываем данные по ключам так же, как и ассоциативные массивы. Приложения состоят из нескольких независимых частей, и если бы все они использовали одно хранилище (например, один каталог на диске), рано или поздно произойдет столкновение ключей. Nette Framework решает эту проблему путем разделения всего пространства на пространства имен (подкаталоги). В этом случае каждая часть программы использует свое собственное пространство с уникальным именем, и коллизии не возникают.

Имя пространства указывается в качестве второго параметра конструктора класса Cache:

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

Теперь мы можем использовать объект $cache для чтения и записи из кэша. Для обоих используется метод load(). Первый аргумент — ключ, а второй — обратный вызов PHP, который вызывается, когда ключ не найден в кэше. Обратный вызов генерирует значение, возвращает его и кэширует:

$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 предоставляет механизм, как ограничить действительность данных и как удалить их контролируемым образом («сделать их недействительными», используя терминологию фреймворка).

Действительность данных устанавливается в момент сохранения с помощью третьего параметра метода save(), например:

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

Или используя параметр $dependencies, переданный по ссылке в обратный вызов в методе 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
$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-страница, содержащая этот тег, удаляется из кэша. То же самое происходит при вставке нового комментария под соответствующей статьей.

Тегам требуется Журнал.

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

Мы можем установить приоритет для отдельных элементов в кэше, и их можно будет удалять контролируемым образом, когда, например, кэш превысит определенный размер:

$dependencies[Cache::Priority] = 50;

Удаляем все элементы с приоритетом, равным или меньшим 100:

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

Приоритетам также требуется Журнал.

Очистка кэша

Параметр Cache::All очищает всё:

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

Массовое чтение

Для массового чтения и записи в кэш используется метод bulkLoad(), в котором мы передаем массив ключей и получаем массив значений:

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

Метод bulkLoad() работает аналогично load() со вторым параметром обратного вызова, которому передается ключ сгенерированного элемента:

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

Кэширование вывода

Выходные данные можно перехватывать и кэшировать очень элегантно:

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

	echo ... // выводим некоторые данные

	$capture->end(); // сохраняем вывод в кэш
}

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

Кэширование в Latte

Кэширование в шаблонах Latte очень легко настраивается, достаточно обернуть часть шаблона тегами {cache}...{/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 данные не хранятся — в целях тестирования

Вы получаете объект хранилища, передавая его с помощью внедрения зависимостей с типом Nette\Caching\Storage. По умолчанию Nette предоставляет объект FileStorage, который хранит данные в подпапке cache в каталоге для временных файлов .

Вы можете изменить хранилище в конфигурации:

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

FileStorage

Записывает кэш в файлы на диске. Хранилище Nette\Caching\Storages\FileStorage очень хорошо оптимизировано для производительности и, прежде всего, обеспечивает полную атомарность операций. Что это значит? Чтобы при использовании кэша не получилось так, что мы читаем файл, который ещё не был полностью записан другим потоком, или чтобы кто-то удалил его «из-под руки». Поэтому использование кэша полностью безопасно.

Это хранилище также имеет важную встроенную функцию, которая предотвращает экстремальное увеличение загрузки процессора, когда кэш очищается или охлаждается (т. е. не создается). Это «профилактика» 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

The SQLite database and adapter Nette\Caching\Storages\SQLiteStorage offer a way to cache in a single file on disk. The configuration will specify the path to this file.

Requires PHP extensions pdo and pdo_sqlite.

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

DevNullStorage

Особой реализацией хранилища является Nette\Caching\Storages\DevNullStorage, которая на самом деле не хранит данные вообще. Поэтому она подходит для тестирования, если мы хотим исключить влияние кэша.

Использование кэша в коде

При использовании кэширования в коде у вас есть два способа, как это сделать. Первый заключается в том, что вы получаете объект хранилища, передавая его с помощью внедрения зависимостей, а затем создаете объект 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
{
	private Nette\Caching\Cache $cache;

	public function __construct(Nette\Caching\Cache $cache)
	{
		$this->cache = $cache;
	}
}

Затем объект Cache создается непосредственно в конфигурации следующим образом:

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

Журнал

Nette хранит теги и приоритеты в так называемом журнале. По умолчанию для этого используются SQLite и файл journal.s3db. Кроме того, требуются PHP расширения pdo и pdo_sqlite.

Вы можете изменить журнал в конфигурации:

services:
	cache.journal: MyJournal