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

Страница каталога в приложении на Yii2 собирается за 400–600 мс, потому что каждый запрос тянет одни и те же справочники и пересчитывает агрегаты. Очевидное решение — обернуть тяжёлый кусок в Yii::$app->cache->set() с временем жизни в час. И тут же приходит вторая проблема: администратор поменял товар, а посетитель ещё сорок минут видит старую цену. Разберём, какие приёмы кеширования в Yii2 реально работают на проде и чем вы платите за каждый.
Всё ниже — про Yii2 (2.0.x). В Yii 1.x API отличается, а Yii3 на момент написания ещё не в стабильном релизе, там компонент кеша построен на PSR-16 и приёмы переносятся не один в один.
Четыре уровня, на которых кешируют в Yii2
Фреймворк даёт не один инструмент, а несколько, и путать их — типичная ошибка. Каждый решает свою задачу:
- Кеш данных —
Yii::$app->cache, методыget/set/getOrSet. Кешируете произвольное значение: результат запроса, массив, объект. - Кеш фрагментов —
beginCache()/endCache()в представлении. Кешируется отрендеренный HTML куска страницы. - Кеш страницы целиком — фильтр
yii\filters\PageCacheна уровне контроллера. - Кеш запросов —
Query::cache()иConnection::cache(), кеширование результата SQL прямо в слое БД.
Инвалидация кеша — отдельная большая тема; здесь разбираем, как кешировать так, чтобы инвалидация вообще стала возможной без ручного перебора ключей.
Кеш данных: getOrSet вместо ручного get/set
Классический код с ручной проверкой выглядит так:
$key = 'catalog-featured';
$data = Yii::$app->cache->get($key);
if ($data === false) {
$data = Product::find()->where(['featured' => 1])->all();
Yii::$app->cache->set($key, $data, 3600);
}
Проблема в том, что false — валидное значение, которое кеш вернуть не может отличить от промаха. И на каждый вызов вы дублируете этот шаблон. В Yii2 для этого есть getOrSet():
// Yii2 2.0.x
$data = Yii::$app->cache->getOrSet('catalog-featured', function () {
return Product::find()->where(['featured' => 1])->all();
}, 3600);
Замыкание вызывается только при промахе, результат сохраняется автоматически. Третий аргумент — время жизни в секундах, четвёртый — зависимость (о ней ниже). Это база, с которой стоит начинать любой новый кусок кеша.
Ключ — не строка наугад
Если в ключе участвуют параметры запроса, не склеивайте их вручную через точку. Передавайте массив — Yii2 сам построит корректный ключ и захеширует его:
$key = ['catalog-list', 'category' => $categoryId, 'page' => $page];
$data = Yii::$app->cache->getOrSet($key, function () use ($categoryId, $page) {
// выборка
});
Так вы не получите коллизию, когда category=1 и page=2 случайно дадут ту же строку, что category=12 и что-то ещё.
Главная боль: инвалидация по времени против зависимостей
Ставить только TTL — самый частый и самый вредный приём. Пользователь видит устаревшие данные ровно столько, сколько указано в TTL, и вы ничего не можете с этим сделать в момент изменения. Правильнее привязывать кеш к зависимости, которая сама сбрасывает его при изменении источника.
DbDependency — когда данные в таблице
yii\caching\DbDependency хранит результат небольшого SQL-запроса и на каждом чтении сравнивает его с сохранённым. Изменилось — кеш считается протухшим:
use yii\caching\DbDependency;
$dependency = new DbDependency([
'sql' => 'SELECT MAX(updated_at) FROM product WHERE category_id = :cat',
'params' => [':cat' => $categoryId],
]);
$data = Yii::$app->cache->getOrSet($key, function () use ($categoryId) {
return Product::find()->where(['category_id' => $categoryId])->all();
}, 3600, $dependency);
Цена: на каждое чтение уходит лишний лёгкий запрос к БД. Он дешевле полной выборки, но не бесплатный. На нагруженном каталоге этот SELECT MAX(...) сам может стать заметным, если по updated_at нет индекса.
TagDependency — когда сбрасывать нужно группами
На практике чаще выигрывает yii\caching\TagDependency. Вы помечаете кеш тегами, а при изменении данных сбрасываете все записи с нужным тегом одной командой — не зная и не перебирая конкретные ключи:
use yii\caching\TagDependency;
// при записи в кеш
$data = Yii::$app->cache->getOrSet($key, function () use ($categoryId) {
return Product::find()->where(['category_id' => $categoryId])->all();
}, 3600, new TagDependency([
'tags' => ['catalog', 'category-' . $categoryId],
]));
// в коде, который меняет товар этой категории
TagDependency::invalidate(Yii::$app->cache, 'category-' . $categoryId);
Один вызов invalidate() обнуляет весь кеш категории независимо от того, сколько там ключей и страниц пагинации. Это то, ради чего теги и придумали.
Не связывайте кеш только со временем. TTL — это страховка от утечки памяти и от забытой инвалидации, а не механизм актуальности. Актуальность обеспечивает зависимость.
Фрагментный кеш и вложенность
Когда тяжёлая часть — рендеринг, а не выборка (сложная разметка меню, дерево категорий), кешируйте фрагмент в представлении:
<?php if ($this->beginCache('main-menu', [
'duration' => 3600,
'dependency' => [
'class' => 'yii\caching\TagDependency',
'tags' => ['menu'],
],
])): ?>
<!-- тяжёлая разметка меню -->
<?php $this->endCache(); endif; ?>
Важная деталь: во фрагментном кеше есть вариации по параметрам через ключ variations — например, кешировать отдельно для каждого языка. Без вариаций гость с английской локалью получит русское меню из кеша.
Ещё один приём — вложенный кеш с динамическими вставками через renderDynamic(). Кешируете всю страницу, но блок «Здравствуйте, Имя» оставляете живым. Синтаксис и ограничения смотрите в разделе Caching руководства Yii2, потому что тонкости с сериализацией замыкания там неочевидны.
Что выбрать под задачу
| Приём | Что кешируется | Когда брать | Чем платите |
|---|---|---|---|
| getOrSet + TagDependency | Результат выборки, массив, объект | Данные меняются точечно, нужен групповой сброс | Логика простановки тегов при записи |
| getOrSet + DbDependency | То же | Нет доступа к коду записи, но есть маркер изменения в БД | Лишний SQL на каждое чтение |
| beginCache / endCache | HTML-фрагмент | Дорогой рендеринг, не выборка | Вариации по языку/роли легко забыть |
| PageCache | Вся страница | Публичные страницы без персонализации | Персонализация ломается, нужен renderDynamic |
| Query::cache() | Результат SQL | Точечное кеширование конкретного запроса | Легко закешировать лишнее, теряете контроль над ключом |
Чем ломается на проде
- FileCache по умолчанию. Из коробки кеш складывается в файлы. На нескольких серверах за балансировщиком у каждого узла свой кеш и своя инвалидация — сброс на одном не долетит до остальных. Переводите на
yii\redis\Cacheилиyii\caching\MemCacheс общим хранилищем. - Теги на FileCache работают, но invalidate не мгновенный. Механизм тегов хранит служебные записи в том же кеше; при разнесённом FileCache сброс тега не распространяется. Ещё один довод за общее хранилище.
- Кеширование ActiveRecord целиком. Объекты AR тянут за собой связи и состояние. При сериализации в кеш вы можете сохранить больше, чем думали. Часто дешевле кешировать
asArray(). - Dogpile при истечении TTL. Когда популярный ключ протух, десятки одновременных запросов кинутся пересчитывать его разом и уронят БД. Yii2 не решает это автоматически — закладывайте блокировку через
Mutexили чуть более раннее обновление. - APCu не шарится с CLI. Если греете кеш консольной командой, а отдаёте через веб на APCu — прогрев не попадёт в веб-процесс. Это разные сегменты памяти.
Что настроить дальше
Начните с замены хранилища на общее (Redis), если приложение уже не на одном сервере, — без этого любые теги и зависимости обманчивы. Затем пройдитесь по местам, где стоит голый TTL, и добавьте TagDependency с осмысленными тегами. Померяйте hit rate: у Redis это redis-cli info stats, поля keyspace_hits и keyspace_misses — если промахов больше половины, кеш либо слишком дробный, либо слишком короткоживущий. И заведите один тег на сущность, чтобы код записи всегда знал, что сбрасывать.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Yii2 Guide — Caching, Yii2 API — yii\caching\TagDependency
Частые вопросы
У меня Yii 1.x, эти приёмы подойдут?
TagDependency::invalidate не сбрасывает кеш, в чём дело?
Что лучше — DbDependency или TagDependency?
Стоит ли кешировать объекты ActiveRecord напрямую?
Как защититься от лавины запросов, когда популярный ключ протух?
Работает ли APCu, если греть кеш из консоли?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.