Кеш
Кеш [keš]
прискорить ваш застосунок, зберігаючи дані, отримані з
великими витратами, для майбутнього використання. Ми покажемо:
- як використовувати кеш
- як змінити сховище
- як правильно інвалідувати кеш
Використання кешу в 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 = /* ... */; // instance of 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}
можна вкладати один в одного, і коли вкладений блок стає
недійсним (наприклад, за допомогою тегу), батьківський блок також стає
недійсним.
У тегу можна вказати ключі, до яких буде прив'язаний кеш (тут змінна
$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
дуже
добре оптимізоване для продуктивності і, перш за все, забезпечує повну
атомарність операцій. Що це означає? Що при використанні кешу не може
статися так, що ми прочитаємо файл, який ще не повністю записаний іншим
потоком, або що хтось його “під руками” видалить. Використання кешу,
таким чином, є абсолютно безпечним.
Це сховище також має вбудовану важливу функцію, яка запобігає екстремальному зростанню використання ЦП у момент, коли кеш видаляється або ще не прогрітий (тобто створений). Це запобігання 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 зберігає теги та пріоритети у так званому журналі. Стандартно для
цього використовується 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 і керують своїм кешем самостійно. Їхній кеш, до речі, не потрібно вимикати в режимі розробки.