Кэширование
Как кэшировать данные, помечать их тегами и сбрасывать группами
Кэш нужен там, где данные достаются дорого, а меняются редко: дерево разделов, счётчики, карта URL, список настроек. Вместо запроса к базе на каждый просмотр страницы результат считается один раз и потом отдаётся из хранилища.
В JohnCMS кэш — это сервис Johncms\Cache\CacheInterface. Внутри работает symfony/cache, но модуль об этом знать не должен: на каком хранилище стоит сайт, решает конфигурация, а код остаётся одинаковым.
Контракт основан на стандарте PSR-16 и добавляет к нему три вещи: remember(), rememberForever() и invalidateTags().
Быстрый старт
Внедрите CacheInterface через конструктор:
<?php
declare(strict_types=1);
namespace Johncms\Modules\MyModule\Application\Services;
use Johncms\Cache\CacheInterface;
use Johncms\Modules\MyModule\Domain\Repository\ArticleRepositoryInterface;
final readonly class PopularArticles
{
/** Тег, под которым лежит всё кэшированное этим сервисом */
private const CACHE_TAG = 'my-module';
public function __construct(
private CacheInterface $cache,
private ArticleRepositoryInterface $repository,
) {
}
/**
* @return array<int, string>
*/
public function titles(): array
{
return $this->cache->remember(
'my_module_popular',
600,
fn (): array => $this->repository->getPopularTitles(),
[self::CACHE_TAG]
);
}
}remember() — основной метод. Если значение в кэше есть, оно возвращается сразу; если нет — выполняется замыкание, результат сохраняется и возвращается. Обращения к базе не будет до тех пор, пока запись не истечёт или её не сбросят по тегу.
Методы
remember($key, $ttl, $callback, $tags)
Отдаёт значение из кэша, а на промахе считает его замыканием и сохраняет
rememberForever($key, $callback, $tags)
То же, но без срока: запись живёт, пока её не сбросят по тегу или не очистят кэш
invalidateTags(...$tags)
Помечает устаревшим всё, что лежит под этими тегами
get($key, $default)
Значение или $default, если записи нет
set($key, $value, $ttl)
Кладёт значение
delete($key)
Удаляет одну запись
has($key)
Есть ли запись
clear()
Очищает весь кэш приложения
getMultiple() / setMultiple() / deleteMultiple()
То же самое пачкой
Всё, кроме первых трёх строк, — стандарт PSR-16.
Методов put() и forget() нет. Класть значение — set(), удалять — delete(). Если вы переносите код со старых версий JohnCMS, где кэш был построен на Laravel, замените их; на каждое действие в контракте оставлено ровно одно имя.
Срок жизни
Третий аргумент remember() — время жизни в секундах либо DateInterval. null означает «без срока» и равнозначен rememberForever().
Выбирайте срок по тому, насколько устаревшие данные допустимы на странице. Счётчик посетителей онлайн живёт десять секунд, счётчик сообщений форума — десять минут. Данные, которые меняются только по действию администратора, обычно кладут без срока и сбрасывают тегом.
Теги: сброс группами
Тег — это метка на записи. Одна запись может нести несколько тегов, а сброс тега делает недействительными все записи под ним, сколько бы их ни было.
Без тегов пришлось бы держать где-то список всех ключей и не забывать дополнять его при добавлении нового. С тегами достаточно пометить новую запись — метод сброса менять не нужно.
Правила:
Тег называется по тому, что за данные, а не откуда они читаются:
news,counters,collections,my-module.Держите тег константой рядом с ключами, которые он покрывает.
Сбрасывайте теги там, где данные меняются, — в use case сохранения и удаления, а не в контроллере.
Сброс по тегу — не удаление. Записи сразу начинают читаться как промах, но место на диске освобождает команда cache:pool:prune, а не сам вызов. Это нормальная работа кэша, но об этом стоит помнить, когда смотрите на размер data/cache.
Ключи и теги
Ключи и теги подчиняются ограничениям PSR-16: символы {}()/\@: запрещены и приводят к исключению Psr\SimpleCache\InvalidArgumentException. Буквы, цифры, точка, дефис и подчёркивание разрешены.
Начинайте ключ с имени модуля — news_subsections, collections_code_map, my_module_popular. Кэш общий на весь сайт, и одинаковые ключи из двух модулей затрут друг друга.
Никогда не подставляйте в ключ то, что ввёл посетитель, — логин, поисковый запрос, адрес страницы. При файловом хранилище ключ становится именем файла, и такой ключ виден в листинге каталога. Если ключ должен зависеть от введённого значения, хэшируйте его:
Что можно класть в кэш
Массивы, скаляры и простые объекты-DTO.
Не кладите в кэш модели Eloquent. Модель сериализуется вместе со связями и состоянием, а после обновления CMS запись прочитается уже в изменённый класс. Приведите данные к массиву или DTO до того, как отдать их в кэш.
Замыкание должно возвращать значение всегда — в том числе когда данных нет. Пустой массив закэшируется и избавит от повторных запросов; null тоже допустим, но тогда remember() при каждом обращении будет считать заново.
Настройка
Кэш настраивается в config/autoload/cache.global.php, а на конкретном сервере переопределяется через cache.local.php (см. Конфигурационные файлы).
Драйверы
filesystem
По умолчанию. Файлы в data/cache/app, работает на любом хостинге
apcu
Быстрее файлового, но память принадлежит одному веб-серверу. Нужно расширение APCu
redis
Установка на нескольких серверах. Нужно расширение redis либо пакет predis/predis
array
Только на время одного запроса. Предназначен для тестов
null
Ничего не хранит, каждое чтение — промах. Удобно при отладке
Теги работают на любом драйвере: даже те хранилища, которые сами тегов не умеют, оборачиваются так, чтобы invalidateTags() действовал.
Для redis политика вытеснения должна быть noeviction или volatile-*. При allkeys-* Redis может вытеснить служебную запись тега, и всё, что под ним лежало, останется висеть как действительное. На такой настройке драйвер откажется работать явной ошибкой, а не молча.
Остальные параметры
namespace— префикс, отделяющий этот сайт от других в общем хранилище. По умолчанию берётся версия CMS, поэтому после обновления кэш начинается с чистого листа и старые записи не читаются.default_lifetime— срок для записей, которые его не задали.0— без ограничения.directory— где хранит файлы драйверfilesystem.null—data/cache/app.tags_storage— как файловый драйвер связывает теги с записями:auto(по умолчанию),symlinkилиfiles. В режимеautoсистема один раз проверяет, разрешает ли хостинг символические ссылки, и запоминает ответ. Менять это нужно только если проверка ошиблась — например, на хостинге, где симлинки создаются, но не работают.
Консольные команды
cache:pool:clear
Очищает кэш приложения — то, что накэшировали модули
cache:pool:clear news counters
Сбрасывает только записи с указанными тегами
cache:pool:prune
Освобождает место, занятое истёкшими и сброшенными записями
cache:clear
Полностью очищает data/cache: кэш приложения, скомпилированный контейнер, маршруты, шаблоны
cache:pool:prune уже стоит в планировщике и выполняется ночью. Если планировщик на сайте не настроен, запускайте её вручную — иначе каталог data/cache будет расти.
Обе команды доступны и через задачи обслуживания в админке, без доступа к консоли.
Тесты
Для тестов есть настоящий кэш в памяти — Tests\Support\InMemoryCache:
Не подменяйте CacheInterface моком: мок метода remember() подтверждает лишь то, что метод вызвали, но не то, что запись получила тег и сбросится вместе с остальными. С настоящим кэшем проверяется поведение — сохранилось, сбросилось, посчиталось заново.
Чего делать не нужно
Перечислять ключи в методе очистки. Для этого есть теги: новый ключ достаточно пометить.
Кэшировать то, что зависит от текущего посетителя, — права, содержимое корзины, персональные списки, — без включения идентификатора в ключ. Кэш общий на весь сайт, и один посетитель увидит данные другого.
Заводить свой файловый кэш (
file_put_contentsвdata/cache). Такой кэш не участвует ни в очистке, ни в сбросе по тегу, и его не видно ни одной из команд.Оборачивать в кэш дешёвый запрос. Чтение записи по первичному ключу обходится дешевле, чем чтение файла кэша и распаковка значения.
Последнее обновление
Это было полезно?