Кэширование в Битриксе редко ломается громко. Оно ломается тихо: контент-менеджер меняет цену, обновляет страницу — цена старая. Меняет ещё раз, чистит кэш кнопкой в админке — стало нормально. Через неделю история повторяется, и в задачник прилетает «сайт глючит».
Почти всегда причина одна: кэш живёт, а зависимость между данными и кэшем нигде не описана. Разберём три механизма, которые для этого есть, и — главное — где проходит граница между «Битрикс сбросит сам» и «сбрасывать должен ты».
Зачем кэширование
Каждый запрос к Битрикс — это десятки SQL-запросов, вызовы API, формирование HTML. Кэширование сохраняет результат и отдаёт его повторно без повторной генерации.
Без кэша: 50–200 запросов к БД, 200–500ms на страницу С кэшем: 0 запросов, 10–30ms на страницу
Виды кэширования в Битрикс:
- Кэш компонентов (управляемый) — автосброс при изменении инфоблоков
- Тегированный кэш (D7) — ручное управление зависимостями
- HTML-кэш (композит) — статические страницы
- memcached/Redis — хранение в памяти
Управляемый кэш компонентов
Стандартный механизм кэширования в Битрикс. Компонент автоматически сбрасывает кэш при изменении данных инфоблока.
Включение в вызове компонента
<?php
$APPLICATION->IncludeComponent(
"bitrix:news.list",
"catalog",
[
"IBLOCK_ID" => 2,
"CACHE_TYPE" => "A", // A = автоматический
"CACHE_TIME" => 3600, // 1 час
"CACHE_GROUPS" => "N", // Не разделять по группам пользователей
]
);Режимы кэширования
| Значение | Описание |
|---|---|
A | Авто — учитывает настройки сайта |
Y | Включено всегда |
N | Выключено |
CACHE_TIME => 0 — это не «без кэша», а «кэш навсегда». Ноль означает бесконечное время жизни: такой кэш живёт до сброса по тегу или до ручной очистки. Ошибка популярная и обидная — человек «отключает кэш», а получает ровно противоположный результат и потом сутки не может понять, почему изменения не появляются.
Отдельно про CACHE_GROUPS. Значение Y заставляет Битрикс хранить отдельную копию кэша для каждой комбинации групп пользователя. Если на сайте есть персональные цены или скрытые от гостей разделы — это необходимость. Если нет — вы просто множите кэш и промахи по нему на пустом месте, поэтому N здесь разумное значение по умолчанию.
Как это работает
1. Компонент генерирует HTML
2. Сохраняет в /bitrix/cache/ с метками (теги инфоблока)
3. При изменении элемента инфоблока — сброс всех кэшей с этим тегомТегированный кэш D7
Более гибкий механизм для собственного кода. Вы сами определяете теги и когда их сбрасывать.
Что реально даёт «автоматический» режим. Компонент сам регистрирует тег инфоблока, и при изменении любого элемента этого инфоблока кэш сбрасывается. На этом автоматика заканчивается: изменения в HL-блоках, во внешних данных, в настройках модулей и в собственном коде она не отслеживает. Всё, что не является элементом инфоблока, вы обязаны сбрасывать сами.
Базовое использование
<?php
use Bitrix\Main\Data\Cache;
use Bitrix\Main\Application;
$cache = Cache::createInstance();
$cacheTime = 3600; // 1 час
$cacheId = 'product_list_' . md5(serialize($filter));
$cacheDir = '/my_cache/products/';
if ($cache->initCache($cacheTime, $cacheId, $cacheDir)) {
// Данные в кэше
$products = $cache->getVars()['products'];
} elseif ($cache->startDataCache()) {
// Генерируем данные
$products = getProductsFromDatabase($filter);
// Добавляем теги для автосброса
$taggedCache = Application::getInstance()->getTaggedCache();
$taggedCache->startTagCache($cacheDir);
$taggedCache->registerTag('iblock_id_2'); // При изменении инфоблока 2
$taggedCache->registerTag('catalog_products'); // Кастомный тег
$taggedCache->endTagCache();
// Сохраняем в кэш
$cache->endDataCache(['products' => $products]);
}Логика конструкции такая: initCache возвращает true, если валидный кэш есть, — тогда данные берутся из getVars(). Иначе startDataCache открывает буфер, вы считаете данные, регистрируете теги и закрываете буфер через endDataCache. Регистрация тегов обязана происходить между startDataCache и endDataCache — за пределами этого блока startTagCache не свяжет тег с конкретной директорией кэша, и сброс по тегу такой кэш не заденет.
Тегированный кэш нужно включить. В настройках модуля main есть опция управляемого кэша (Использовать тегированный кэш), и при выключенной опции весь код с registerTag отработает без единой ошибки — просто ничего не сделает. Прежде чем часами искать, почему clearByTag не срабатывает, проверьте эту галочку и параметр cache_flags в настройках.
Сброс по тегу
<?php
use Bitrix\Main\Application;
$taggedCache = Application::getInstance()->getTaggedCache();
// Сбросить всё с тегом catalog_products
$taggedCache->clearByTag('catalog_products');
// Сбросить кэш конкретного инфоблока
$taggedCache->clearByTag('iblock_id_2');
// Сбросить всё
\BXClearCache(true);Последняя строка — тот случай, когда «работает» и «правильно» расходятся. Полный сброс кэша на живом сайте означает, что все страницы одновременно начнут генерироваться заново; на нагруженном каталоге это гарантированная лавина запросов к базе и вполне реальный шанс уронить проект собственными руками. BXClearCache(true) уместен после деплоя в момент низкого трафика — но не в обработчике события, который срабатывает при каждом сохранении товара.
Практический пример: кэширование каталога
<?php
namespace Local\Catalog;
use Bitrix\Main\Data\Cache;
use Bitrix\Main\Application;
use Bitrix\Iblock\ElementTable;
class CachedProductRepository
{
private const CACHE_DIR = '/local/products/';
private const CACHE_TIME = 3600;
private const IBLOCK_ID = 2;
public function getList(array $filter = [], int $limit = 20): array
{
$cache = Cache::createInstance();
$cacheId = $this->buildCacheId($filter, $limit);
if ($cache->initCache(self::CACHE_TIME, $cacheId, self::CACHE_DIR)) {
// ?? [] — страховка: кэш мог быть записан кодом предыдущей версии,
// где набор ключей отличался, и тогда 'items' в нём не окажется
return $cache->getVars()['items'] ?? [];
}
if ($cache->startDataCache()) {
$items = $this->fetchFromDatabase($filter, $limit);
// Регистрируем теги
$this->registerCacheTags();
$cache->endDataCache(['items' => $items]);
return $items;
}
return [];
}
public function getById(int $id): ?array
{
$cache = Cache::createInstance();
$cacheId = 'product_' . $id;
if ($cache->initCache(self::CACHE_TIME, $cacheId, self::CACHE_DIR)) {
return $cache->getVars()['item'];
}
if ($cache->startDataCache()) {
$item = $this->fetchOneFromDatabase($id);
$taggedCache = Application::getInstance()->getTaggedCache();
$taggedCache->startTagCache(self::CACHE_DIR);
$taggedCache->registerTag('iblock_id_' . self::IBLOCK_ID);
$taggedCache->registerTag('product_' . $id); // Тег конкретного товара
$taggedCache->endTagCache();
$cache->endDataCache(['item' => $item]);
return $item;
}
return null;
}
/**
* Сброс кэша конкретного товара
*/
public function clearProductCache(int $productId): void
{
$taggedCache = Application::getInstance()->getTaggedCache();
$taggedCache->clearByTag('product_' . $productId);
}
/**
* Сброс всего кэша каталога
*/
public function clearAllCache(): void
{
$taggedCache = Application::getInstance()->getTaggedCache();
$taggedCache->clearByTag('iblock_id_' . self::IBLOCK_ID);
}
private function buildCacheId(array $filter, int $limit): string
{
return 'list_' . md5(serialize($filter) . '_' . $limit);
}
private function registerCacheTags(): void
{
$taggedCache = Application::getInstance()->getTaggedCache();
$taggedCache->startTagCache(self::CACHE_DIR);
$taggedCache->registerTag('iblock_id_' . self::IBLOCK_ID);
$taggedCache->endTagCache();
}
private function fetchFromDatabase(array $filter, int $limit): array
{
// Реальный запрос к БД
return [];
}
private function fetchOneFromDatabase(int $id): ?array
{
return null;
}
}О чём стоит подумать при выборе cacheId. Здесь он собирается из md5(serialize($filter)) — это правильный подход: любой параметр, влияющий на результат выборки, обязан входить в ключ. Забытый параметр приводит к самому неприятному классу багов, когда одному пользователю показывается результат чужого запроса. Обратная крайность тоже вредна: если сунуть в ключ время или идентификатор сессии, кэш перестанет попадать вообще.
Автосброс при изменениях
Обработчик событий инфоблока
<?php
// /bitrix/php_interface/init.php
use Bitrix\Main\EventManager;
use Bitrix\Main\Application;
$eventManager = EventManager::getInstance();
// При изменении элемента инфоблока
$eventManager->addEventHandler('iblock', 'OnAfterIBlockElementUpdate', function ($arFields) {
$taggedCache = Application::getInstance()->getTaggedCache();
// Сбрасываем кэш конкретного элемента
$taggedCache->clearByTag('product_' . $arFields['ID']);
// В событиях обновления приходят только изменённые поля,
// поэтому проверяем наличие ключа, а не его значение
if (isset($arFields['ACTIVE'])) {
$taggedCache->clearByTag('iblock_id_' . $arFields['IBLOCK_ID']);
}
});
// При добавлении/удалении элемента
$eventManager->addEventHandler('iblock', 'OnAfterIBlockElementAdd', function ($arFields) {
$taggedCache = Application::getInstance()->getTaggedCache();
$taggedCache->clearByTag('iblock_id_' . $arFields['IBLOCK_ID']);
});
$eventManager->addEventHandler('iblock', 'OnAfterIBlockElementDelete', function ($arFields) {
$taggedCache = Application::getInstance()->getTaggedCache();
$taggedCache->clearByTag('iblock_id_' . $arFields['IBLOCK_ID']);
$taggedCache->clearByTag('product_' . $arFields['ID']);
});Цены не приходят в события инфоблока. Цена товара в торговом каталоге живёт не в свойствах элемента, а в отдельной таблице модуля catalog, и меняется она своими событиями (OnPriceUpdate, OnPriceAdd, события Bitrix\Catalog\Model\Price). Обработчик OnAfterIBlockElementUpdate при изменении цены через админку или обмен с 1С попросту не сработает — и вы получите классическое «в каталоге старая цена, в карточке новая». Если кэшируете цены, вешайте обработчик и на события модуля каталога.
Обработчики событий — это код в критическом пути записи. Он выполняется внутри сохранения элемента, а значит, при обмене с 1С отработает на каждой из десятков тысяч позиций. Держите такие обработчики предельно лёгкими: сброс тега — это операция с файлами или обращение к Redis, и на массовом импорте она из «мелочи» превращается в основную статью расходов.
Кэш в memcached / Redis
Для высоконагруженных проектов файловый кэш заменяют на in-memory хранилища.
Настройка Redis
<?php
// /bitrix/.settings_extra.php
return [
'cache' => [
'value' => [
'type' => [
'class_name' => '\\Bitrix\\Main\\Data\\CacheEngineRedis',
'extension' => 'redis',
],
'redis' => [
'host' => '127.0.0.1',
'port' => 6379,
],
'sid' => $_SERVER['DOCUMENT_ROOT'] . '#01',
],
],
];Почему Redis, а не memcached. Дело не в скорости — на типовых нагрузках разницы вы не заметите. Дело в том, что PHP-расширение memcache (без «d») давно не развивается, а на нескольких веб-серверах общий Redis решает вопрос согласованности кэша между ними. Конфигурацию memcached ниже привожу для проектов, где он уже стоит и работает; для нового проекта выбор очевиден.
Настройка memcached
<?php
// /bitrix/.settings_extra.php
return [
'cache' => [
'value' => [
'type' => [
'class_name' => '\\Bitrix\\Main\\Data\\CacheEngineMemcache',
'extension' => 'memcache',
],
'memcache' => [
'host' => '127.0.0.1',
'port' => 11211,
],
'sid' => $_SERVER['DOCUMENT_ROOT'] . '#01',
],
],
];Отладка проблем
Кэш не сбрасывается
Проверяйте по порядку — список отсортирован по тому, как часто именно этот пункт оказывается виноват:
- Включён ли тегированный кэш в настройках модуля
main. Без него весь код с тегами работает вхолостую. - Регистрируются ли теги внутри блока
startDataCache/endDataCache. Снаружи вызов ничего не связывает. - Тот ли тег сбрасывается. Опечатка в строке тега не вызывает ошибки — тег просто никогда не совпадёт.
- Права на
/bitrix/cache/и/bitrix/managed_cache/. После переноса сайта под другим пользователем кэш нередко пишется, но не удаляется. - Не отдаёт ли страницу композит или nginx. Ситуация «сбросил кэш Битрикса, а страница прежняя» чаще всего означает, что до PHP запрос вообще не дошёл.
<?php
// Отладка тегов
$taggedCache = Application::getInstance()->getTaggedCache();
// Логируем все сбросы
file_put_contents(
$_SERVER['DOCUMENT_ROOT'] . '/cache_debug.log',
date('Y-m-d H:i:s') . " Clearing tag: {$tag}\n",
FILE_APPEND
);Кэш не работает
<?php
// Проверка работы кэша
$cache = Cache::createInstance();
if ($cache->initCache(60, 'test_cache', '/test/')) {
echo 'FROM CACHE: ' . $cache->getVars()['time'];
} elseif ($cache->startDataCache()) {
$time = time();
$cache->endDataCache(['time' => $time]);
echo 'GENERATED: ' . $time;
}
// Если при обновлении время не меняется — кэш работаетИтоги
| Метод | Когда использовать |
|---|---|
| Кэш компонентов | Стандартные компоненты Битрикс |
| Тегированный кэш | Собственный код, гибкие зависимости |
| Композит | Анонимные страницы каталога |
| Redis | Несколько веб-серверов, highload |
Главный вывод, к которому я пришёл после нескольких проектов с «глючащим» кэшем: сложность кэширования не в том, чтобы данные сохранить, а в том, чтобы честно описать, от чего они зависят. Всё остальное — детали API.
Что из этого следует на практике:
- Сначала опишите зависимости, потом включайте кэш. Вопрос «что должно измениться, чтобы эти данные устарели» задаётся до написания кода, а не после первого бага.
- Сбрасывайте по тегам, а не целиком. Полная очистка на живом трафике — это отказ в обслуживании, устроенный самому себе.
- Не кэшируйте персональное вместе с общим. Персональные цены, содержимое корзины, признак авторизации в общем кэше однажды покажут одному пользователю данные другого. Это не гипотетический риск — это регулярно случающийся инцидент.
- Помните про
CACHE_TIME = 0— бесконечно, а не выключено. - Следите за объёмом
/bitrix/cache/. На большом каталоге с разбивкой по группам он способен съесть диск, а закончившееся место ломает сессии и роняет сайт целиком.
Частые вопросы
Что означает CACHE_TIME = 0 в компоненте Битрикса?
Не «выключить кэш», а «кэшировать бесконечно». Ноль означает неограниченное время жизни: такой кэш живёт до сброса по тегу или до ручной очистки. Ошибка популярная и обидная — человек «отключает кэш», получает ровно противоположный результат и потом сутки не может понять, почему изменения не появляются. Чтобы кэш действительно отключить, ставят CACHE_TYPE = N.
Почему clearByTag не сбрасывает кэш?
Проверяйте по порядку. Включён ли тегированный кэш в настройках модуля main — при выключенной опции весь код с registerTag отработает без ошибок и просто ничего не сделает. Регистрируются ли теги внутри блока startDataCache и endDataCache, а не снаружи. Совпадает ли строка тега — опечатка не вызывает ошибки, тег просто никогда не совпадёт. Есть ли права на /bitrix/cache/ и /bitrix/managed_cache/. И не отдаёт ли страницу композит или nginx, минуя PHP.
Почему после изменения цены в каталоге остаётся старая?
Потому что цены не приходят в события инфоблока. Цена товара в торговом каталоге лежит не в свойствах элемента, а в отдельной таблице модуля catalog, и меняется своими событиями — OnPriceUpdate, OnPriceAdd и событиями Bitrix\Catalog\Model\Price. Обработчик OnAfterIBlockElementUpdate при изменении цены через админку или обмен с 1С попросту не сработает, отсюда классическое «в каталоге старая цена, в карточке новая».
Что реально даёт автоматический режим кэширования компонента?
Компонент сам регистрирует тег инфоблока, и при изменении любого элемента этого инфоблока кэш сбрасывается. На этом автоматика заканчивается: изменения в HL-блоках, во внешних данных, в настройках модулей и в собственном коде она не отслеживает. Всё, что не является элементом инфоблока, вы обязаны сбрасывать сами.
Стоит ли вызывать BXClearCache(true) при сохранении товара?
Нет. Полный сброс кэша на живом сайте означает, что все страницы одновременно начнут генерироваться заново; на нагруженном каталоге это гарантированная лавина запросов к базе и реальный шанс уронить проект собственными руками. Полная очистка уместна после деплоя в момент низкого трафика, но не в обработчике события, который срабатывает при каждом сохранении элемента.
Redis или memcached для кэша Битрикса?
Redis. Дело не в скорости — на типовых нагрузках разницы вы не заметите. Дело в том, что PHP-расширение memcache без «d» давно не развивается, а на нескольких веб-серверах общий Redis решает вопрос согласованности кэша между ними. Конфигурация memcached оправдана только там, где он уже стоит и работает.