Кеш в Битрикс D7 — это не «магическая галочка в админке», а явный контракт: ключ, TTL, теги инвалидации и место хранения. Без тегов вы либо живёте с устаревшими данными, либо сбрасываете всё подряд. Ниже — рабочий минимум Bitrix\Main\Data\Cache, TaggedCache и managed cache: когда что брать, как не словить гонки и типичные ошибки в модулях.
Связка с остальным D7 естественная: сервис из ServiceLocator отдаёт Result, тяжёлый кусок оборачиваете в Cache, а HTTP к внешним API — через HttpClient с отдельным TTL.
Три уровня: Cache, TaggedCache, managed
Bitrix\Main\Data\Cache— базовый init/start/end по строковому ключу и каталогу. Подходит для «посчитал раз — положил на N секунд».Bitrix\Main\Data\TaggedCache— те же данные, но с тегами. После изменения сущности сбрасываете тег, а не весь кеш сайта.- Managed cache (
Application::getInstance()->getManagedCache()) — обёртка над тем же движком с удобным API для компонентов и длительных кусков; часто используется ядром.
Движок (файлы, Redis, Memcached) задаётся в настройках главного модуля. Код модуля должен быть агностичен к бэкенду: вы работаете с ключом и TTL, не с путями на диске.
Минимальный паттерн init / start / end
<?php
use Bitrix\Main\Data\Cache;
final class ProductCardCache
{
private const TTL = 3600;
private const DIR = '/local/product_card';
public function get(int $productId): array
{
$cache = Cache::createInstance();
$key = 'product_card_' . $productId;
if ($cache->initCache(self::TTL, $key, self::DIR)) {
return (array)$cache->getVars();
}
if ($cache->startDataCache()) {
$data = $this->loadHeavy($productId);
if ($data === []) {
// пустой результат не кешируем
$cache->abortDataCache();
return [];
}
$cache->endDataCache($data);
return $data;
}
return [];
}
private function loadHeavy(int $productId): array
{
// ORM / внешний API / сборка карточки
return [
'id' => $productId,
'name' => 'Demo',
'price' => 1990,
];
}
}
Правила, которые экономят часы отладки:
- Ключ должен однозначно кодировать входные параметры (ID, сайт, язык, группа цен). Разные пользователи с разной ценой — разные ключи или отдельный слой персонализации вне кеша.
abortDataCache()— когда результат нельзя класть (ошибка, пусто, «ещё не готово»). Иначе вы закешируете дыру на весь TTL.- Каталог (
$initDir) группирует файлы/ключи. Сброс каталога проще, чем охота за отдельными ключами вручную.
TaggedCache: инвалидация по сущности
Теги нужны, когда TTL «наугад» не подходит: товар изменили в админке — карточка должна обновиться сразу, а соседние сущности жить дальше.
<?php
use Bitrix\Main\Application;
use Bitrix\Main\Data\Cache;
final class CatalogSectionCache
{
private const TTL = 86400;
private const DIR = '/local/catalog_section';
public function getSectionPayload(int $sectionId, string $siteId): array
{
$cache = Cache::createInstance();
$tagged = Application::getInstance()->getTaggedCache();
$key = sprintf('section_%s_%d', $siteId, $sectionId);
if ($cache->initCache(self::TTL, $key, self::DIR)) {
return (array)$cache->getVars();
}
if ($cache->startDataCache()) {
$tagged->startTagCache(self::DIR);
$payload = $this->build($sectionId, $siteId);
// теги сущности и «глобальный» тег раздела каталога
$tagged->registerTag('iblock_id_5');
$tagged->registerTag('section_' . $sectionId);
if ($payload === []) {
$tagged->abortTagCache();
$cache->abortDataCache();
return [];
}
$tagged->endTagCache();
$cache->endDataCache($payload);
return $payload;
}
return [];
}
public function onSectionChanged(int $sectionId): void
{
Application::getInstance()->getTaggedCache()->clearByTag('section_' . $sectionId);
}
private function build(int $sectionId, string $siteId): array
{
return ['sectionId' => $sectionId, 'siteId' => $siteId, 'items' => []];
}
}
Типичная схема в модуле: при сохранении элемента/раздела/настройки вызываете clearByTag(...) из обработчика EventManager. Не сбрасывайте весь кеш сайта из агента «на всякий случай» — это убивает hit rate и диск/Redis.
Managed cache для сервисов
<?php
use Bitrix\Main\Application;
$managed = Application::getInstance()->getManagedCache();
$cacheId = 'rates_cbr_' . date('YmdH');
if ($managed->read(1800, $cacheId)) {
$rates = $managed->get($cacheId);
} else {
$rates = fetchRatesFromApi(); // ваш HttpClient
$managed->set($cacheId, $rates);
}
Managed удобен, когда не нужен ручной startDataCache/endDataCache, а нужен короткий read/set. Для сложной сборки с несколькими тегами обычно явнее классический Cache + TaggedCache.
Что класть в кеш, а что нет
- Можно: собранные DTO/массивы для витрины, результаты тяжёлых SELECT, ответы внешних API с понятным TTL, HTML-фрагменты без персональных данных.
- Нельзя / опасно: объекты с ресурсами (PDO, открытые файлы), данные текущего пользователя без ключа по userId, секреты и токены, сырой HTML с CSRF/персональными блоками «Здравствуйте, Иван».
- Осторожно: огромные массивы на десятки МБ — вынесете давление на Redis/диск и сериализацию. Лучше кешировать агрегат «топ-50», а детали подгружать точечно.
Гонки, stampede и пустые значения
Когда TTL истекает одновременно у многих запросов, все бегут в БД/API (cache stampede). Практичные приёмы:
- чуть размазывать TTL:
baseTtl + ($id % 60); - не кешировать пустой результат надолго (или кешировать «пустышку» на 30–60 секунд, чтобы не долбить источник);
- тяжёлую пересборку выносить в агент/очередь, а веб отдавать last-known-good;
- при ошибке источника —
abortDataCache(), а не запись фейла на час.
Сброс: точечно, по тегу, по каталогу
<?php
use Bitrix\Main\Application;
use Bitrix\Main\Data\Cache;
// по тегу (предпочтительно)
Application::getInstance()->getTaggedCache()->clearByTag('product_42');
// весь каталог ключей модуля
Cache::clearCache(true, '/local/product_card');
// managed
$managed = Application::getInstance()->getManagedCache();
$managed->clean('rates_cbr_2026090910');
В админке «Очистить кеш» — аварийный рубильник после деплоя или инцидента, не часть бизнес-логики модуля. В коде модуля опирайтесь на теги и узкие каталоги.
Типичные ошибки
- Один ключ на всех: забыли сайт, язык, валюту или группу цен — пользователи видят чужие цены.
- Кеш ошибки: API упал, вы записали пустой массив на сутки.
- Теги без clearByTag: зарегистрировали тег, но в OnAfterUpdate его не чистите — TTL становится единственным способом обновления.
- Кеш внутри транзакции: записали данные до commit, транзакцию откатили — в кеше ложь.
- Персональные блоки в HTML-кеше компонента: авторизованный кусок должен жить вне общего кеша или с ключом по userId (обычно дороже, чем ESI/отдельный ajax).
- Бесконечный TTL «потому что редко меняется»: без тегов вы получите зомби-данные после редкого, но важного обновления.
Мини-чеклист для модуля
- Ключ кодирует все входные параметры, влияющие на результат.
- TTL разумный; критичные сущности дополнительно на тегах.
- Пустой/ошибочный результат не пишется на длинный TTL.
- Обработчики событий чистят теги после изменения данных.
- Нет секретов и персоналки в общем кеше.
- После деплоя ломающих изменений — точечный clear по каталогу модуля, не «снести весь сайт» из крона каждый час.
Компонентный кеш и кеш в PHP-сервисе
У компонентов Битрикс свой слой (cacheable, CACHE_TIME, автосоздание ключа из параметров). Он удобен для шаблонов, но бизнес-логику модуля лучше кешировать в PHP-сервисе: ключ и теги контролируете вы, а не косвенные параметры вызова. Компонент тогда либо читает уже готовый DTO из сервиса, либо кеширует только презентацию.
Если оставить тяжёлый SQL внутри component.php под стандартным кешем компонента, инвалидация через ваши теги модуля усложняется: придётся знать внутренний каталог/ключ компонента или снова жать «очистить кеш». Явный сервис + TaggedCache проще сопровождать.
Отладка: почему отдаётся старое
- Проверьте, что обработчик события реально вызывается (лог в EventManager) и вызывает
clearByTagс тем же именем тега, что вregisterTag. - Сверьте ключ: лишний пробел, другой
SITE_ID, другая валюта — и вы читаете соседнюю запись. - Убедитесь, что на проде тот же cache engine, что на стейдже (файлы vs Redis). После смены бэкенда старые ключи «невидимы».
- Временно поставьте TTL 10–30 секунд на проблемном месте или добавьте в payload поле
cachedAt— сразу видно, свежий ли ответ. - Не мешайте HTML-кеш CDN/nginx с кешем приложения без плана: можно сбросить PHP-кеш и всё равно отдавать старую страницу с края.
Вывод
Cache даёт TTL и ключ, TaggedCache — управляемую инвалидацию, managed — короткий read/set. В модулях на D7 держите кеш рядом с сервисом (через ServiceLocator), возвращайте наружу Result с понятными ошибками источника и сбрасывайте данные тегами, а не глобальной кнопкой. Так hit rate растёт, а «почему на витрине старая цена» перестаёт быть квестом.
