Кеширане

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

  • Как да използвате кеша
  • Как да промените съхранението на кеша
  • Как да обявите кеша за невалиден правилно

Използването на кеша в 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 решава този проблем, като разделя цялото пространство на пространства от имена (поддиректории). В този случай всяка част от програмата използва свое собствено пространство с уникално име и не се получават колизии.

Името на пространството се задава като втори параметър на конструктора на класа 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 предоставя механизъм за ограничаване на валидността на данните и за тяхното изтриване по контролиран начин (“обезсилване” според терминологията на рамката).

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

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

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

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

Или като използвате третия параметър в метода 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 е много добре оптимизиран за производителност и преди всичко осигурява пълна атомичност на операциите. Какво означава това? За да не се окаже, че при използване на кеша ще прочетем файл, който все още не е бил изцяло записан от друга нишка, или че някой го е изтрил “на ръка”. Ето защо използването на кеша е напълно безопасно.

Това хранилище има и важна вградена функция, която предотвратява екстремно увеличаване на натоварването на процесора, когато кешът е изчистен или охладен (т.е. не е създаден). Това е “профилактично” “избутване на кеша:https://en.wikipedia.org/…he_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')

ПаметЗапаметяване

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') )

Log

Nette съхранява етикетите и приоритетите в т.нар. регистрационен файл. По подразбиране се използва SQLite и файлът journal.s3db. Освен това разширенията на PHP pdo и pdo_sqlite са **задължителни.

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

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