# Кэширование в 1С-Битрикс — управляемый и тегированный кэш

URL: https://dmeshcheryakov.ru/blog/bitrix-cache/
Раздел: 1С-Битрикс
Теги: 1С-Битрикс, Кэширование, Performance
Опубликовано: 2026-08-31
Обновлено: 2026-09-07
Автор: Дмитрий Мещеряков (https://dmeshcheryakov.ru)

> Полный гайд по кэшированию: управляемый кэш компонентов, тегированный кэш D7, автосброс при изменениях. Избавляемся от проблемы 'а почему не обновилось?'

Кэширование в Битриксе редко ломается громко. Оно ломается тихо: контент-менеджер меняет цену, обновляет страницу — цена старая. Меняет ещё раз, чистит кэш кнопкой в админке — стало нормально. Через неделю история повторяется, и в задачник прилетает «сайт глючит».

Почти всегда причина одна: кэш живёт, а зависимость между данными и кэшем нигде не описана. Разберём три механизма, которые для этого есть, и — главное — где проходит граница между «Битрикс сбросит сам» и «сбрасывать должен ты».

## Зачем кэширование

Каждый запрос к Битрикс — это десятки SQL-запросов, вызовы API, формирование HTML. Кэширование сохраняет результат и отдаёт его повторно без повторной генерации.

**Без кэша:** 50–200 запросов к БД, 200–500ms на страницу
**С кэшем:** 0 запросов, 10–30ms на страницу

**Виды кэширования в Битрикс:**
1. Кэш компонентов (управляемый) — автосброс при изменении инфоблоков
2. Тегированный кэш (D7) — ручное управление зависимостями
3. HTML-кэш (композит) — статические страницы
4. memcached/Redis — хранение в памяти

## Управляемый кэш компонентов

Стандартный механизм кэширования в Битрикс. Компонент автоматически сбрасывает кэш при изменении данных инфоблока.

### Включение в вызове компонента

```php
<?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
<?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
<?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
<?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
<?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
<?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
<?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',
        ],
    ],
];
```

## Отладка проблем

### Кэш не сбрасывается

Проверяйте по порядку — список отсортирован по тому, как часто именно этот пункт оказывается виноват:

1. **Включён ли тегированный кэш** в настройках модуля `main`. Без него весь код с тегами работает вхолостую.
2. **Регистрируются ли теги внутри блока `startDataCache` / `endDataCache`.** Снаружи вызов ничего не связывает.
3. **Тот ли тег сбрасывается.** Опечатка в строке тега не вызывает ошибки — тег просто никогда не совпадёт.
4. **Права на `/bitrix/cache/` и `/bitrix/managed_cache/`.** После переноса сайта под другим пользователем кэш нередко пишется, но не удаляется.
5. **Не отдаёт ли страницу композит или nginx.** Ситуация «сбросил кэш Битрикса, а страница прежняя» чаще всего означает, что до PHP запрос вообще не дошёл.

```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
<?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/`.** На большом каталоге с разбивкой по группам он способен съесть диск, а закончившееся место ломает сессии и роняет сайт целиком.