Кеш

Кеш [кеш |Cache] ускоряет ваше приложение, сохраняя данные, полученные с трудом один раз, для последующего использования. Мы покажем вам:

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

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

Установка

Скачать и установить библиотеку можно с помощью Composer:

composer require nette/caching

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

Центром работы с кешем является объект Nette\Caching\Cache. Мы создаем его экземпляр и передаем конструктору так называемое хранилище. Это объект, представляющий место, где данные будут физически храниться (база данных, Memcached, файлы на диске, …). К хранилищу мы получаем доступ, запросив его с помощью внедрения зависимостей с типом 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 предлагает механизм для ограничения срока действия данных или их управляемого удаления (в терминологии фреймворка — «инвалидации»).

Срок действия данных устанавливается в момент сохранения с помощью третьего параметра метода 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). Теги {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

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

Требуются расширения PHP pdo и 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
{
	public function __construct(
		private Nette\Caching\Cache $cache,
	) {
	}
}

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

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

Journal

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

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

services:
	cache.journal: MyJournal

Сервисы DI

Эти сервисы добавляются в DI-контейнер:

Название Тип Описание
cache.journal Nette\Caching\Storages\Journal журнал
cache.storage Nette\Caching\Storage хранилище

Отключение кеша

Одним из способов отключения кеша в приложении является установка в качестве хранилища DevNullStorage:

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

Эта настройка не влияет на кеширование шаблонов в Latte или DI-контейнера, поскольку эти библиотеки не используют сервисы nette/caching и управляют своим кешем самостоятельно. Их кеш, впрочем, нет необходимости отключать в режиме разработки.

версия: 3.x