Кеш Symfony: прогрев контейнера и инвалидация тегов
Прогрев кеша Symfony на деплое и инвалидация закешированных ответов — две разные задачи, которые часто путают. Разбираем, чем отличается cache:warmup от инвалидации через теги и где всё это ломается на проде.

«Кеш Symfony» — это не одна сущность. Есть кеш контейнера и роутинга, который собирается командой cache:warmup при деплое, и есть кеш данных приложения — HTTP-ответы, результаты запросов, фрагменты шаблонов, — который живёт в пуле через компонент symfony/cache. Первый нужно прогреть один раз при выкладке, второй — инвалидировать точечно, когда данные изменились. Путаница между ними стоит либо отдачи устаревших страниц пользователям, либо стопки 500-х на первом запросе после релиза.
Статья про два конкретных механизма: прогрев системного кеша при деплое и инвалидацию прикладного кеша через теги. Разбор для Symfony 6.4 LTS и 7.x — API кеша между этими версиями стабилен. Инвалидация HTTP-кеша на уровне обратного прокси (Varnish, символьный HttpCache) — отдельная тема, здесь её не касаемся.
Два кеша, которые не путать
Прежде чем говорить о прогреве и инвалидации, стоит развести понятия. От того, какой кеш вы имеете в виду, зависит и команда, и стратегия.
| Свойство | Системный кеш | Прикладной кеш (cache pool) |
|---|---|---|
| Что хранит | Скомпилированный контейнер, роуты, аннотации, метаданные Doctrine | Ответы, результаты запросов, фрагменты, ваши данные |
| Где лежит | var/cache/{env}/ | Пул: файлы, Redis, APCu, БД |
| Как прогревается | bin/console cache:warmup | Лениво — при первом запросе, или отдельным скриптом |
| Как сбрасывается | cache:clear при деплое | По ключу, по тегу или cache:pool:clear |
| Частота изменений | Раз на деплой | Постоянно, по мере изменения данных |
cache:warmup к вашим закешированным ответам API отношения не имеет. Он не «прогреет» Redis с результатами запросов. Это первое, обо что спотыкаются, когда после релиза страница отдаёт старые данные и кто-то запускает cache:warmup в надежде на чудо.
Прогрев системного кеша на деплое
Проблема, которую решает прогрев: если выложить код и очистить var/cache, то первый пользователь после релиза заплатит за компиляцию контейнера. На тяжёлом приложении это несколько секунд, и если запросов много, компиляция может пойти в несколько потоков одновременно и упереться в блокировки файлов.
Правильный порядок на деплое — очистить и прогреть до того, как трафик пойдёт на новый релиз:
# Symfony 6.4 / 7.x, prod-окружение
# APP_ENV должен быть выставлен в prod
export APP_ENV=prod
# 1. Очистка старого кеша
bin/console cache:clear --no-warmup
# 2. Прогрев: компиляция контейнера, роутов, метаданных
bin/console cache:warmup
# 3. Оптимизация автозагрузчика Composer
composer dump-autoload --optimize --no-dev --classmap-authoritativeФлаг --no-warmup у cache:clear нужен, чтобы очистка не запустила прогрев автоматически с половиной ещё не готового окружения. Прогрев запускаете отдельным шагом, когда всё на месте.
Деплой без даунтайма: releases и симлинки
Стандартная схема Deployer и подобных инструментов: код каждого релиза раскладывается в отдельную папку releases/N, а current — симлинк на активный релиз. Тонкость с кешем Symfony в том, что абсолютные пути внутри скомпилированного контейнера привязываются к директории, где шёл прогрев.
Прогревайте кеш в той же директории, откуда приложение будет запускаться после переключения симлинка. Прогрев вreleases/42и запуск черезcurrentдают разные абсолютные пути — контейнер может ссылаться на несуществующие файлы.
Практический вывод: либо прогревайте после переключения симлинка, либо настройте прогрев так, чтобы он использовал финальный путь. У Symfony есть переменная окружения для кастомного пути кеша — уточните актуальное имя (обычно связанное с APP_CACHE_DIR или переопределением getCacheDir() в Kernel) в документации вашей версии, потому что механизм переопределения менялся.
Инвалидация прикладного кеша через теги
Теперь про кеш данных. Допустим, вы кешируете список товаров категории и карточку каждого товара. Товар отредактировали — нужно сбросить и карточку, и все списки, где он мог появиться. Держать список ключей вручную нереально. Для этого есть тегируемый кеш.
Тегируемый пул реализует Symfony\Contracts\Cache\TagAwareCacheInterface. Вы помечаете записи тегами при сохранении, а потом инвалидируете сразу все записи с нужным тегом одним вызовом.
<?php
// src/Service/ProductCache.php
// Symfony 6.4 / 7.x, symfony/cache
namespace App\Service;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
final class ProductCache
{
public function __construct(
private readonly TagAwareCacheInterface $cache,
) {
}
public function getProduct(int $productId): array
{
return $this->cache->get(
"product_{$productId}",
function (ItemInterface $item) use ($productId): array {
$item->expiresAfter(3600);
// Тег на конкретный товар и на всю категорию
$item->tag(["product_{$productId}", 'products']);
return $this->loadProductFromDb($productId);
}
);
}
// Сбросить один товар и все списки товаров
public function invalidateProduct(int $productId): void
{
$this->cache->invalidateTags([
"product_{$productId}",
'products',
]);
}
private function loadProductFromDb(int $productId): array
{
// ... запрос к БД
return [];
}
}Чтобы получить тегируемый пул, в config/packages/cache.yaml нужен пул с включёнными тегами:
# config/packages/cache.yaml
framework:
cache:
# адаптер по умолчанию для прикладного кеша
app: cache.adapter.redis
default_redis_provider: '%env(REDIS_DSN)%'
pools:
product.cache:
adapter: cache.adapter.redis
tags: trueПосле этого Symfony зарегистрирует сервис, который можно типизировать через TagAwareCacheInterface с привязкой к пулу. Имя аргумента для автовайринга конкретного пула — уточните в документации компонента Cache вашей версии, синтаксис привязки пулов к именованным сервисам иногда меняется.
Как работают теги внутри
Тег — не магия. Для каждого тега пул хранит служебную запись с версией. Когда вы вызываете invalidateTags, версия тега увеличивается. При чтении элемента пул сверяет версии его тегов с текущими: если хоть одна не совпала — элемент считается протухшим и пересчитывается. То есть invalidateTags не удаляет данные немедленно, а делает их невалидными при следующем чтении.
Отсюда важное следствие для Redis: тегируемый кеш делает дополнительные обращения на сверку версий тегов. На горячем пути с сотнями чтений в секунду это заметная нагрузка на Redis. Если тегов у записи много, число round-trip растёт.
Где это ломается
- APCu и теги в prod с несколькими воркерами. APCu живёт в памяти одного процесса. Инвалидация тега в одном PHP-FPM воркере не видна другим. Для распределённой инвалидации нужен общий бэкенд — Redis. APCu годится только под локальный кеш, который не требует синхронной инвалидации между процессами.
- Инвалидация после деплоя обнуляет прикладной кеш. Если пул хранится в
var/cache(файловый адаптер),cache:clearсотрёт и его. После релиза первый трафик пойдёт по холодному кешу — стадо запросов ударит в БД. Держите прикладной кеш в отдельном хранилище (Redis), которое переживает деплой. - Гонка при пересчёте (cache stampede). Когда популярный ключ протухает, десятки параллельных запросов одновременно кинутся его пересчитывать. Метод
get()с колбэком поддерживает механизм ранней пересборки через параметрbetaвexpiresAfter/черезItemInterface— вероятностное досрочное обновление. Уточните текущую сигнатуру в документации, но сам приём: не давать всем запросам протухнуть в одну секунду. - Теги множатся бесконтрольно. Тег
productsна каждой записи означает, что одна правка товара инвалидирует весь каталог. Иногда это нужно, чаще — нет. Проектируйте гранулярность тегов под реальные сценарии изменения данных.
Что делать дальше
Разнесите два кеша окончательно: системный — в var/cache, прогревается на деплое; прикладной — в Redis, инвалидируется тегами и деплой переживает. Проверьте это командой bin/console cache:pool:list — она покажет зарегистрированные пулы.
На проде мерьте две вещи: время первого запроса после релиза (показатель, что прогрев отработал) и hit ratio прикладного пула в Redis (INFO stats в redis-cli, отношение keyspace_hits к сумме hits и misses). Резкое падение hit ratio после деплоя — сигнал, что прикладной кеш случайно очищается вместе с системным. Дальше стоит изучить раздел про cache stampede и раннюю пересборку в документации компонента Cache, если у вас есть горячие ключи.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Symfony Cache Component — официальная документация, Cache Invalidation with Tags — Symfony docs
Частые вопросы
У меня Symfony 5.4, а не 6.4 — примеры подойдут?
После cache:warmup при деплое ловлю 500 — в чём дело?
invalidateTags вызвал, а старые данные всё равно отдаются
Нужно ли вообще прогревать кеш на маленьком приложении?
Чем cache:clear отличается от cache:pool:clear?
Теги сильно грузят Redis — стоит их бояться?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.