Кеширование в Yii2: зависимости и инвалидация по тегам
Кеш живёт до истечения TTL, а данные меняются раньше — и пользователь видит устаревшую страницу. Разбираем зависимости кеша в Yii2 и инвалидацию по тегам через TagDependency: когда что выбирать и где это ломается.

Знакомая история: вы закешировали список товаров на час, менеджер поменял цену через админку, а посетитель ещё сорок минут видит старую. Классический TTL тут не помогает — он не знает, что данные изменились. В Yii2 для этого есть зависимости кеша (dependencies): объект, который умеет сказать «данные протухли» по внешнему признаку, а не по таймеру. Разберём, чем отличаются встроенные зависимости, как работает инвалидация по тегам через TagDependency и в каких местах это тихо перестаёт работать.
Материал для Yii2 (2.0.x). В Yii3 API кеша переписан на PSR-16/PSR-6 и работает иначе — здесь речь только про Yii2.
Что такое зависимость и как она подключается
Зависимость — это объект-наследник yii\caching\Dependency, который передаётся третьим аргументом в set(). При каждом get() кеш проверяет: изменилось ли то, от чего зависит запись. Если да — значение считается устаревшим и get() вернёт false, как будто записи нет.
use yii\caching\DbDependency;
// закешировать на час, но сбросить, если в таблице появились новые заказы
$dependency = new DbDependency([
'sql' => 'SELECT MAX(updated_at) FROM {{%product}}',
]);
Yii::$app->cache->set('product-list', $products, 3600, $dependency);
Механика простая: в момент set() зависимость вычисляет и запоминает «слепок» (для DbDependency — результат SQL). При get() слепок пересчитывается и сравнивается. Не совпал — кеш инвалидируется.
Встроенные зависимости Yii2
yii\caching\DbDependency— слепок берётся из результата SQL-запроса. Удобно, но каждыйget()порождает запрос к БД. Кеш перестаёт разгружать базу так сильно, как хотелось бы.yii\caching\FileDependency— зависит от времени модификации файла. Подходит для конфигов и сгенерированных ассетов.yii\caching\ExpressionDependency— слепок из результата вычисления PHP-выражения.yii\caching\ChainedDependency— комбинирует несколько зависимостей по И или ИЛИ (флагdependOnAll).yii\caching\TagDependency— зависимость по строковым тегам, которые вы инвалидируете вручную. Главный герой статьи.
Инвалидация по тегам: TagDependency
Проблема DbDependency — она реактивна: чтобы понять, устарел ли кеш, надо сходить в базу. TagDependency работает наоборот. Вы помечаете записи тегами, а когда данные меняются — сами объявляете тег недействительным одной командой. Все записи с этим тегом протухают одновременно.
use yii\caching\TagDependency;
// при записи вешаем теги
Yii::$app->cache->set(
'product-42',
$product,
3600,
new TagDependency(['tags' => ['products', 'product-42']])
);
Yii::$app->cache->set(
'product-list',
$products,
3600,
new TagDependency(['tags' => 'products'])
);
// при изменении любого товара сбрасываем всё, помеченное 'products'
TagDependency::invalidate(Yii::$app->cache, 'products');
После invalidate() и product-42, и product-list при следующем get() вернут false. Записи физически остаются в хранилище — они просто помечены как недействительные и будут перезаписаны при следующем сохранении. Это важно понимать для оценки объёма кеша: invalidate() не освобождает память сразу.
Тег в Yii2 — это не удаление ключей, а сдвиг «версии» тега. Каждая запись хранит версию тега на момент записи;invalidate()просто пишет новую версию, и старые записи перестают совпадать. Поэтому одинinvalidate()дешёвый независимо от того, тысяча записей под тегом или одна.
Как работает версионирование тегов внутри
При set() с TagDependency Yii для каждого тега кладёт в тот же кеш служебный ключ с текущей меткой версии (генерируется как случайное значение). В саму запись зашивается снимок этих меток. На get() движок сравнивает сохранённые метки с текущими. invalidate() перезаписывает служебный ключ новой меткой — снимок в старых записях перестаёт совпадать.
Отсюда неочевидное следствие: служебные ключи тегов живут в том же хранилище, что и данные. Если хранилище вытеснит служебный ключ (например, Memcached по LRU выкинул его как редко используемый), Yii сочтёт метку отсутствующей — и все записи под этим тегом посчитаются устаревшими. Кеш «протухнет» без вашего invalidate().
Когда что выбирать
| Ситуация | Что брать | Цена |
|---|---|---|
| Данные меняются через ваш код (админка, CRUD) | TagDependency | Надо не забыть вызвать invalidate во всех точках записи |
| Данные меняет внешний процесс, в код которого не влезть | DbDependency | Запрос к БД на каждый get |
| Зависимость от файла (конфиг, сборка фронта) | FileDependency | stat файла на каждый get |
| Несколько условий сразу | ChainedDependency | Суммируется стоимость всех вложенных |
| Кеш можно просто держать по таймеру | Без зависимости, только TTL | Риск показать устаревшее до истечения TTL |
Практический критерий: если событие изменения данных проходит через ваш PHP-код — берите теги, инвалидация будет мгновенной и без нагрузки на базу. Если данные меняются мимо приложения (репликация, другой сервис, крон на другом языке) — теги вызвать негде, тут спасает DbDependency или короткий TTL.
Теги в реальном проекте: где их вешать
Удобная схема — три уровня тегов на сущность:
- Тег коллекции —
products. Сбрасывает все списки и агрегаты по товарам. Инвалидируется при создании и удалении. - Тег экземпляра —
product-42. Сбрасывает всё, что относится к конкретному товару. Инвалидируется при его изменении. - Тег связей —
category-7, если запись зависит и от категории.
use yii\caching\TagDependency;
use yii\base\Event;
// в модели Product — сброс тегов после сохранения и удаления
public function afterSave($insert, $changedAttributes)
{
parent::afterSave($insert, $changedAttributes);
$tags = ['product-' . $this->id];
if ($insert) {
// новый товар меняет и списки
$tags[] = 'products';
}
TagDependency::invalidate(Yii::$app->cache, $tags);
}
public function afterDelete()
{
parent::afterDelete();
TagDependency::invalidate(Yii::$app->cache, ['products', 'product-' . $this->id]);
}
Ключевая дисциплина: инвалидация должна жить рядом с записью данных, а не разбросана по контроллерам. Иначе однажды кто-то добавит массовое обновление через updateAll() — а он не дёргает afterSave() — и кеш останется висеть со старыми данными.
Где это ломается
Массовые операции в обход событий
Product::updateAll(['price' => 100], ['category_id' => 7]) не вызывает afterSave(). Кеш не сбросится. Такие места надо помнить и вызывать invalidate() вручную сразу после массового запроса.
Разные бэкенды кеша под одним тегом
Теги работают в пределах одного компонента кеша. Если списки лежат в cache (Redis), а фрагменты — в frontCache (файловый), invalidate($redis, 'products') файловый кеш не тронет. Вызывать надо для каждого компонента отдельно.
Вытеснение служебных ключей
Описано выше: LRU-вытеснение служебной метки тега приводит к массовому «протуханию». В Memcached с плотно забитой памятью это реальный сценарий. Если наблюдаете, что кеш неожиданно холодный без явных invalidate() — проверьте политику вытеснения и объём памяти хранилища.
Гонка при инвалидации и записи
Между invalidate() и следующим set() есть окно, когда параллельный запрос перечитывает данные и кеширует их. При высокой параллельности это приводит к cache stampede — толпа запросов одновременно бьёт в базу за одним ключом. Yii2 частично закрывает это через getOrSet() с зависимостью, но полноценной блокировки на перестроение из коробки нет — под нагрузкой смотрите в сторону мьютекса (yii\mutex\Mutex) вокруг перестроения тяжёлого кеша.
Проверка, что теги действительно сбрасывают
Быстрый способ убедиться в консоли Yii (./yii в интерактивном режиме или во временном контроллере):
use yii\caching\TagDependency;
$cache = Yii::$app->cache;
$dep = new TagDependency(['tags' => 'test-tag']);
$cache->set('k', 'value', 0, $dep);
var_dump($cache->get('k')); // string(5) "value"
TagDependency::invalidate($cache, 'test-tag');
var_dump($cache->get('k')); // bool(false) — тег сработал
Если после invalidate() всё ещё возвращается значение — проверьте, что set() и invalidate() идут в один и тот же компонент кеша.
Что делать дальше
Сначала соберите карту точек записи для каждой кешируемой сущности: где данные создаются, меняются и удаляются — и убедитесь, что в каждой висит invalidate() с правильными тегами. Отдельно найдите все updateAll(), deleteAll() и прямые SQL — это места, где события молчат. Затем определите, в каком хранилище кеша хватает памяти под служебные ключи тегов, и настройте мониторинг hit-rate: резкое падение при отсутствии деплоя — сигнал вытеснения или гонки. Прогрев кеша и защита от stampede через мьютекс — отдельная тема, здесь мы разбирали только зависимости и теги.
Инфобез и 152-ФЗ: Аудит политик конфиденциальности, правила сбора cookie.
Спросить за 20 ₽Источники: Yii2 Guide — Caching: Cache Dependencies, Yii2 API — yii\caching\TagDependency
Частые вопросы
Работает ли TagDependency в Yii3?
Если invalidate() вызван, а кеш всё равно отдаёт старое?
Освобождает ли invalidate() память в Redis или Memcached?
Сколько тегов можно повесить на одну запись?
Чем DbDependency хуже тегов, если она тоже сбрасывает по изменению данных?
Что происходит при высокой параллельности в момент инвалидации?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.