Кеширование в Laravel: теги и инвалидация без сюрпризов
Теги кеша выглядят удобно, пока не окажется, что ваш драйвер их не поддерживает, а flush по тегу стирает больше, чем вы думали. Разбираем, как теги работают внутри и когда они ломаются.

Классическая ситуация: карточка товара кешируется по ключу product:42, список категории — по category:electronics, а фрагмент главной — по homepage:featured. Меняется цена одного товара, и вам нужно сбросить все три записи разом. Перебирать ключи руками — путь к рассогласованию: где-то забыли, где-то ключ собрали с другим суффиксом. Теги кеша решают ровно эту задачу: помечаете записи ярлыком и сбрасываете их одной командой. Проблема в том, что теги в Laravel работают не на любом драйвере и ведут себя не так буквально, как читается в документации.
Здесь разбираем механизм тегов и инвалидации в Laravel 11.x/12.x: где они хранятся, почему flush по тегу стирает не то, что кажется, и какие драйверы теги не поддерживают вовсе.
Где теги вообще работают
Первое, обо что спотыкаются: теги поддерживают не все драйверы кеша. Официально tagging доступен только на драйверах, которые умеют хранить дополнительные метаданные о принадлежности ключа к тегу.
| Драйвер | Теги | Комментарий |
|---|---|---|
redis | Да | Рекомендуемый выбор для тегов в проде |
memcached | Да | Поддерживается, но у Memcached нет TTL-сканирования по множеству |
array | Да | Только в рамках одного запроса, для тестов |
file | Нет | Бросит BadMethodCallException при вызове tags() |
database | Нет | То же — метод tags() недоступен |
Если в .env у вас CACHE_STORE=file (значение по умолчанию в свежем Laravel до настройки), любой вызов Cache::tags(...) уронит приложение исключением BadMethodCallException: This cache store does not support tagging. Это не баг — это ограничение драйвера. Проверьте актуальный список поддерживаемых драйверов в разделе Cache Tags документации вашей версии, прежде чем строить архитектуру на тегах.
Как теги хранятся в Redis
Тег — это не поле у записи. Под капотом Laravel для каждого тега создаёт отдельное множество (Redis set), куда складывает ссылки на ключи, помеченные этим тегом. Когда вы кешируете значение с тегами, происходит примерно следующее: генерируется уникальный внутренний ключ, значение пишется под ним, а во множества всех переданных тегов добавляется ссылка на этот ключ.
Практическое следствие: настоящий ключ в Redis выглядит не как product:42, а как длинная строка с префиксом тега и хешем. Поэтому кеш, записанный с тегами, нельзя прочитать без тегов — это будут физически разные ключи.
Значение, записанное черезCache::tags(['products'])->put('product:42', ...), не достанется черезCache::get('product:42'). Читать нужно тем же набором тегов, каким писали. Это самая частая причина «кеш не работает, хотя я его положил».
Базовые операции
Записываем значение под одним или несколькими тегами и читаем его тем же набором тегов.
use Illuminate\Support\Facades\Cache;
// Laravel 11.x, драйвер redis
// Кладём карточку товара под двумя тегами
Cache::tags(['products', 'category:electronics'])
->put('product:42', $product, now()->addHours(6));
// Читаем — обязательно с тем же набором тегов
$product = Cache::tags(['products', 'category:electronics'])
->get('product:42');
// remember: если нет — посчитать и положить
$product = Cache::tags(['products', 'category:electronics'])
->remember('product:42', now()->addHours(6), function () {
return Product::find(42);
});Проверка, что работает: после первого remember в Redis появится ключ значения и записи в множествах тегов products и category:electronics. Второй вызов той же строки не пойдёт в базу — замыкание не выполнится.
Инвалидация: flush по тегу
Ключевая операция ради которой всё и затевалось — сброс группы записей одной командой:
// Сбросить все записи с тегом products
Cache::tags(['products'])->flush();
// Сбросить пересечение? Нет. Это объединение.
Cache::tags(['products', 'orders'])->flush();Вот здесь прячется главная ловушка. Cache::tags(['products', 'orders'])->flush() сбрасывает записи, помеченные тегом products или тегом orders — это объединение, а не пересечение. Если вы рассчитывали удалить только записи, у которых есть оба тега, вы удалите гораздо больше.
Где это ломается на практике
Теги притягивают три типа ошибок, и все три всплывают под нагрузкой или при масштабировании.
- Осиротевшие ключи. Когда истекает TTL самого значения, ссылка на него во множестве тега не удаляется автоматически синхронно. Множество тега со временем распухает ссылками на давно исчезнувшие ключи. На проекте с высокой ротацией кеша это заметно раздувает память Redis.
- Flush не атомарен по смыслу. Сброс тега — это операция над множеством, а не мгновенное удаление всех значений одной командой. В момент между началом и концом сброса возможны гонки: параллельный запрос успеет прочитать ещё живое значение или, наоборот, пересчитать и записать в уже сбрасываемый тег.
- Смешение тегового и обычного доступа. Разработчик кладёт с тегами, а коллега читает без — и получает промах. На код-ревью это ловится по несовпадению набора тегов между
put/rememberиget.
Что писать в замечании на ревью
Не «проверьте кеширование», а конкретно: «Значение кладётся с тегами ['products', 'category:X'], а в ReportController читается через Cache::get() без тегов — это гарантированный промах, унифицируйте набор тегов или вынесите его в константу.»
Точечная инвалидация вместо flush по тегу
Если задача — сбросить именно одну карточку, а не всю группу, теги для этого избыточны и опасны широтой захвата. Часто разумнее держать плоский ключ и удалять его адресно:
// Точечный сброс одной записи
Cache::forget('product:42');
// Пересчёт по событию модели
Product::updated(function (Product $product) {
Cache::forget("product:{$product->id}");
});Теги выигрывают там, где записей в группе много и перечислить их ключи заранее невозможно — например, «сбросить весь кеш этой категории, сколько бы товаров в ней ни было». Для сброса единичной записи forget предсказуемее и не трогает соседей.
Когда что выбирать
- Плоские ключи + forget — когда вы точно знаете, какую запись сбрасываете, и их немного. Драйвер любой, включая
fileиdatabase. - Теги + flush — когда нужно сбросить группу неизвестного заранее состава и вы на Redis/Memcached. Цена — распухание множеств и риск задеть лишнее при объединении тегов.
Что делать дальше
Первым делом проверьте CACHE_STORE в конфиге: если там file или database, а в коде есть tags(), приложение упадёт на первом же обращении — переводите кеш на Redis. Дальше настройте мониторинг памяти Redis: смотрите на рост размера множеств тегов через SCARD по вашим тегам — если множество растёт, а активных ключей мало, у вас копятся осиротевшие ссылки. И заведите константы или enum для наборов тегов, чтобы put и get нельзя было рассинхронизировать руками. Инвалидация по событиям моделей — отдельная тема, здесь мы разбирали только механику тегов и flush.
Видеомейкер: Ролик по описанию или оживление вашего фото. От пяти секунд, со звуком, вертикально или горизонтально.
Спросить за 190 ₽Источники: Laravel Cache — официальная документация
Частые вопросы
Почему кеш не читается, хотя я его точно положил?
А если у меня драйвер file или database?
flush по двум тегам удаляет записи с обоими тегами или с любым?
Почему память Redis растёт, хотя ключи с TTL истекают?
Отличается ли поведение тегов между версиями Laravel?
Что безопаснее для сброса одной записи — тег или forget?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.