Кешування

Кеш прискорює роботу вашого додатка, зберігаючи дані – одного разу насилу витягнуті – для використання в майбутньому. Ми покажемо вам:

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

Використання кешу в 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] = '/путь/до/data.yaml';
// або
$dependencies[Cache::Files] = ['/путь/до/data1.yaml', '/путь/до/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;
});

Використання з 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') )

Журнал

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

Ви можете змінити журнал у конфігурації:

services:
	cache.journal: MyJournal

Послуги з проведення розслідувань

Ці сервіси додаються до контейнера 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