Кэширование

11 / 21
15 мин чтения
Введение

В прошлой части мы разбирали события и асинхронность: что происходит после важного действия и в каком процессе это должно жить.

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

Разбор опирается на актуальную документацию Laravel 12.x и исходники ядра Bitrix main (Cache / ManagedCache / TaggedCache). Там, где поведение критично, приводятся имена классов и настройки, чтобы факт можно было проверить.

В статье:

  • Laravel Cache facade: stores, remember, flexible (stale-while-revalidate), tags, locks;
  • Bitrix: unmanaged Cache, ManagedCache, TaggedCache, ORM query cache, кэш компонентов, Composite Site;
  • как выбрать уровень кэша и как не сломать персональные данные и права доступа;
  • типичные ошибки инвалидации и карта «когда какой инструмент».

Кэш: что именно мы сохраняем

Кэш — это осознанный компромисс: мы платим памятью/диском/Redis за то, чтобы не повторять дорогую операцию.

Три разных «объекта кэширования», которые часто путают:

  1. Данные — результат запроса, DTO, список ID. Инвалидируется при изменении источника.
  2. Фрагмент UI — HTML блока/компонента. Зависит от параметров, языка, групп пользователя.
  3. Страница целиком — полный HTML ответа. Самый быстрый hit, самые жёсткие ограничения по персонализации.

Хороший кэш отвечает на три вопроса до написания кода:

  • Что кэшируем (данные / фрагмент / страница)?
  • Ключ включает все влияющие параметры (фильтр, сайт, язык, права)?
  • Кто и когда сбрасывает (TTL, тег, событие, cleanCache)?

Если на третий вопрос ответа нет — это не кэш, а отложенный баг.

Laravel: один API — много backends

В Laravel кэш — единый слой через facade Cache (или контракт Illuminate\Contracts\Cache\Repository). Конфигурация — config/cache.php, default store задаётся через CACHE_STORE / .env.

Поддерживаемые backends из коробки: database, file, redis, memcached, dynamodb, array, null. С Laravel 11+ дефолтом часто становится database (таблица cache из миграции) — для production обычно переходят на Redis.

Документация: Cache.

Базовый цикл: get / put / remember

        use Illuminate\Support\Facades\Cache;

// Прочитать
$posts = Cache::get('posts.latest');

// Записать на час
Cache::put('posts.latest', $posts, now()->addHour());

// Классика: прочитать или вычислить и сохранить
$posts = Cache::remember('posts.latest', 3600, function () {
    return Post::query()
        ->where('active', true)
        ->latest()
        ->limit(20)
        ->get();
});

    

remember — главный DX-выигрыш Laravel: один вызов вместо «if hit / else compute / store». Для «навсегда до явного сброса» есть rememberForever + forget / flush.

Stale-while-revalidate: Cache::flexible

Когда TTL истекает, первый пользователь после протухания часто «платит» полным пересчётом. Cache::flexible реализует паттерн stale-while-revalidate.

ℹ️ Инфо Stale-while-revalidate — стратегия кэша: пока значение «чуть устарело», его всё равно отдают сразу, а свежий пересчёт делают в фоне.

Обычный remember так не умеет: после истечения TTL первый пользователь всегда ждёт полный пересчёт. Cache::flexible как раз про этот компромисс:

        $posts = Cache::flexible('posts.latest', [300, 600], function () {
    return Post::query()->latest()->limit(20)->get();
});

    
  • до 300 секунд — значение fresh, отдаём сразу;
  • между 300 и 600 — отдаём stale, а пересчёт уходит в deferred-функцию после ответа;
  • после 600 — значение expired, пересчитываем синхронно. Таймлайн для ключа: Это особенно полезно для тяжёлых списков и агрегатов, где краткая «слегка устаревшая» выдача лучше, чем спайк latency.
  1. Fresh (0–300 сек) — кэш свежий, отдаём как есть, ничего не пересчитываем.
  2. Stale (300–600 сек) — формальный TTL вышел, но ещё «льготное» окно: пользователю отдаём старое значение сразу, а пересчёт уходит в deferred-функцию после ответа.
  3. Expired (после 600 сек) — льготное окно тоже кончилось: пересчитываем синхронно, пользователь ждёт.

Зачем: убрать latency-спайк у первого запроса после протухания.

Минус: короткое время можно отдать слегка устаревшие данные — для ленты/витрины часто ок, для остатков, цен и прав — нет.

Теги

Теги позволяют сбросить группу ключей одной операцией:

        Cache::tags(['posts', 'homepage'])->remember('posts.latest', 3600, function () {
    return Post::latest()->limit(20)->get();
});

// После обновления поста:
Cache::tags(['posts'])->flush();

    
⚠️ Важное ограничение
tags не поддерживаются драйверами file, database и dynamodb. На практике tags = Redis или Memcached. Если в .env остался database — код с tags упадёт или будет недоступен.

Атомарные блокировки

Для «только один процесс пересчитывает кэш» Laravel даёт locks:

        $lock = Cache::lock('rebuild-sitemap', 10);

if ($lock->get()) {
    try {
        $this->rebuildSitemap();
    } finally {
        $lock->release();
    }
}

    

Или block(5) — ждать до 5 секунд. Это отдельный класс задач: защита от thundering herd и гонок при прогреве кэша. В Bitrix отдельного first-class Cache Lock API такого уровня нет.

Несколько stores и memoization

        Cache::store('redis')->put('metrics.online', 42, 60);

// В рамках одного HTTP-запроса — не ходить в Redis повторно:
$value = Cache::memo()->get('posts.latest');

    

Cache::memo() держит значения в памяти текущего процесса — удобно, когда один и тот же ключ читается из нескольких слоёв за запрос.

Bitrix: несколько уровней кэша — и это норма

В Bitrix нет одного facade «на все случаи». Есть лестница уровней, и сила платформы как раз в том, что каждый уровень решает свой класс задач.

Уровень Класс / механизм Типичный кейс
Unmanaged Bitrix\Main\Data\Cache Результат выборки/сервиса с TTL
Managed ManagedCache Редко меняющиеся данные, жизнь до явного clean
Tagged TaggedCache Инвалидация группы записей по тегу
ORM ['cache' => ['ttl' => …]] + cleanCache() Кэш getList на уровне сущности
Component startResultCache / CACHE_* HTML/данные компонента
Composite Bitrix\Main\Composite\Engine Полная HTML-страница

Конфигурация движка — секция cache в .settings.php (files / memcache / redis / … + sid как префикс ключей).

Unmanaged Cache: классический шаблон

        use Bitrix\Main\Data\Cache;

$cache = Cache::createInstance();
$ttl = 3600;
$cacheId = 'posts_latest_' . md5(serialize($filter));
$cacheDir = '/my_shop/posts';

if ($cache->initCache($ttl, $cacheId, $cacheDir))
{
    $data = $cache->getVars();
}
elseif ($cache->startDataCache())
{
    $data = PostTable::getList([
        'filter' => $filter,
        'select' => ['ID', 'TITLE', 'DATE_CREATE'],
        'order'  => ['ID' => 'DESC'],
        'limit'  => 20,
    ])->fetchAll();

    if ($data === [])
    {
        $cache->abortDataCache();
    }
    else
    {
        $cache->endDataCache($data);
    }
}

    

Ключевые детали:

  • cacheId обязан включать все переменные, влияющие на результат;
  • cacheDir — «папка» для выборочной очистки (cleanDir);
  • пустой/ошибочный результат — abortDataCache(), иначе вы закэшируете «дыру».

Это многословнее Cache::remember, но контракт явный: hit → miss → start → end/abort.

TaggedCache: инвалидация по смыслу, а не по ключу

        use Bitrix\Main\Application;
use Bitrix\Main\Data\Cache;

$cache = Cache::createInstance();
$cacheDir = '/my_shop/posts';
$cacheId = 'posts_latest';

$taggedCache = Application::getInstance()->getTaggedCache();

if ($cache->initCache(3600, $cacheId, $cacheDir))
{
    $data = $cache->getVars();
}
elseif ($cache->startDataCache())
{
    $taggedCache->startTagCache($cacheDir);
    $taggedCache->registerTag('my_shop_posts');
    $taggedCache->registerTag('my_shop_post_list');

    $data = $this->fetchPosts();

    if ($data === [])
    {
        $taggedCache->abortTagCache();
        $cache->abortDataCache();
    }
    else
    {
        $taggedCache->endTagCache();
        $cache->endDataCache($data);
    }
}

// В обработчике OnAfterUpdate / после сохранения:
Application::getInstance()->getTaggedCache()->clearByTag('my_shop_posts');

    

Теги живут в таблице связей (CacheTagTable / b_cache_tag) и работают поверх выбранного cache engine. Это важное отличие от Laravel: в Bitrix tags не привязаны к «только Redis».

Свои теги именуйте явно (my_shop_posts). Автоматических тегов вида ORM_<TABLE> нет — это частый миф.

ORM query cache

        $rows = PostTable::getList([
    'select' => ['ID', 'TITLE'],
    'filter' => ['=ACTIVE' => 'Y'],
    'cache'  => [
        'ttl' => 3600,
        'cache_joins' => true,
    ],
])->fetchAll();

// Сброс кэша сущности:
PostTable::cleanCache();
// или:
PostTable::getEntity()->cleanCache();

    

ORM кладёт автокэш в ManagedCache-директории вида orm_<table_name>. Записи, меняющие строки, обычно сами вызывают cleanCache() на уровне ORM. Для HTML/компонентных кэшей связанные ваши теги сбрасывайте в event handler'ах отдельно.

Кэш компонента

        if ($this->startResultCache(false, [
    (string)($this->arParams['SECTION_ID'] ?? ''),
    $USER->GetGroups(),
]))
{
    $this->arResult['ITEMS'] = $this->fetchItems();
    $this->includeComponentTemplate();
}

    

Параметры:

  • CACHE_TYPEA / Y / N;
  • CACHE_TIME — TTL;
  • CACHE_GROUPS — учитывать группы пользователя в ключе.

Компонентный кэш — это уже не «сервисный remember», а кэш результата UI-блока. Именно поэтому в ключе должны быть права и параметры отображения.

Composite Site: кэш целой страницы

Composite — штатный HTML-кэш страницы: статическая оболочка отдаётся быстро (в т.ч. через nginx), динамические зоны догружаются отдельно.

Ограничения, которые нельзя игнорировать:

  • персональные данные не должны попадать в статическую часть;
  • корзина, имя пользователя, непрочитанные — в dynamic frame;
  • без проверки dynamic-зон Composite превращает баг персонализации в «кэш показал чужое».

В Laravel аналога «нажми в админке и получи page cache платформы» нет: обычно это reverse proxy (Varnish/Cloudflare), Cache-Control или пакеты поверх response. Это не минус фреймворка как такового, но в сравнении «из коробки для CMS» Bitrix здесь глубже.

Инвалидация: TTL vs теги vs события

Стратегия Когда уместна Риск
Только TTL Редко меняющиеся публичные данные, допустима «лаг»-свежесть Пользователь видит старое до истечения TTL
Теги / flush по группе Списки, карточки, связанные блоки Забыли зарегистрировать тег — «вечный» stale
Событие записи → clean / clearByTag Каталог, новости, карточки товара Handler не зарегистрирован на uninstall/install
ORM cleanCache() Кэш getList сущности Путают с TaggedCache и ждут несуществующий ORM_* тег

Практическое правило: TTL — страховка, тег/событие — источник истины. TTL без инвалидации допустим для витрины «обновится в течение часа». Для остатков склада, цен и прав доступа — нет.

Связка с прошлой частью серии: тяжёлую пересборку кэша (прогрев, регенерация sitemap, массовый clearByTag + warm-up) часто уводят в очередь / agent / addBackgroundJob, а не держат в HTTP-запросе редактора.

Карта выбора

Laravel

  • Простой результат сервиса → Cache::remember.
  • Тяжёлый агрегат, важна latency → Cache::flexible.
  • Группа связанных ключей → Cache::tags([...]) на Redis/Memcached.
  • Защита от гонок при пересчёте → Cache::lock.
  • Полная страница → CDN / reverse proxy / HTTP cache headers (не «встроенный Composite»).

Bitrix

  • Результат сервиса/репозитория → Cache::createInstance() + TTL + свой cacheDir.
  • Редко меняющиеся справочники → ManagedCache + явный clean.
  • Список/карточка с инвалидацией из админки/событий → TaggedCache + clearByTag.
  • Повторяющиеся getList → ORM cache.ttl + Table::cleanCache().
  • UI-блок на странице → кэш компонента (startResultCache, CACHE_GROUPS).
  • Публичная витрина под нагрузкой → Composite + корректные dynamic-зоны.

Сравнительная таблица: кэширование

Шкала: от −2 до +2. Балл снимается только при технически обоснованном пробеле — не за «непривычность» API.

Критерий Laravel Bitrix Комментарий
DX прикладного data-cache +2 +1 Laravel: remember / flexible / memo — короткий выразительный слой. Bitrix: initCache / startDataCache / endDataCache многословнее, но контракт hit/miss/abort явный и предсказуемый.
Тегированная инвалидация +1 +2 Laravel tags мощные, но только на Redis/Memcached (не на file/database). Bitrix TaggedCache + clearByTag штатно работает с таблицей тегов поверх выбранного engine.
Слои платформы (ORM / UI / page) +1 +2 Laravel отлично закрывает data-cache; view/page — обычно своими силами или proxy. Bitrix даёт ORM cache, кэш компонентов и Composite как части платформы.
Full-page HTML cache «из коробки» 0 +2 У Laravel нет встроенного аналога Composite Site. Bitrix закрывает витринный page cache админкой + nginx-сценариями, при дисциплине dynamic-зон.
Locks, SWR, memoization +2 0 Cache::lock, Cache::flexible, Cache::memo — first-class инструменты Laravel. В Bitrix эти паттерны собирают вручную (если вообще нужны).
Production-драйверы (Redis/Memcached/files) +2 +2 Оба стека нормально живут на Redis/Memcached; оба умеют file-backend для простых окружений. Паритет.
Итого за статью +8 +9
Общий счёт (накопительный) +96 +64 Счёт накапливается по мере выхода статей.
🛠 Практическая работа

Один список — две стратегии инвалидации

Цель — почувствовать разницу между TTL-only и tag invalidation, и не забыть про ключ.

Сценарий общий: публичный список последних 20 публикаций. Редактор меняет заголовок — список на витрине должен обновляться предсказуемо.

Laravel

  1. Store — redis (или memcached): tags обязательны для этого упражнения.
  2. Сервис PostListService::latest(): Collection внутри использует Cache::tags()
  3. В модели/observer Post на saved / deleted: flush()
  4. Проверьте:
    • первый запрос — miss + запись;
    • второй — hit;
    • после правки заголовка — список обновился без ожидания TTL;
    • на CACHE_STORE=database tags недоступны (осознанно сломайте и верните Redis).

Документация: Cache, Cache Tags.

Bitrix

  1. Вынесите выборку в сервис модуля, оберните в Cache + TaggedCache с cacheDir = '/my_shop/posts' и тегом my_shop_posts_list.
  2. Зарегистрируйте handler на событие сохранения элемента/записи сущности: clearByTag('my_shop_posts_list').
  3. Отдельно включите ORM-cache на getList с ttl и проверьте, что после update помогает PostTable::cleanCache() — это другой слой, не замена TaggedCache для HTML.
  4. Опционально: выведите список компонентом с startResultCache и CACHE_GROUPS=Y, сравните ключ для гостя и для админа.
  5. Проверьте:
    • hit/miss unmanaged-кэша;
    • сброс по тегу после сохранения;
    • что пустая выборка не залипла (abortDataCache).

Документация: Кэширование (и разделы по компонентам / композиту в документации продукта).

Вывод

Laravel делает data-cache предметом минутной работы: один facade, remember, tags на Redis, locks и flexible для борьбы с latency-спайками. Это лучший DX, если кэш — про результаты сервисов и API.

Bitrix делает кэш многослойной платформенной дисциплиной: unmanaged для сервисов, tags для смысловой инвалидации, ORM cache для запросов, component cache для UI, Composite — для страницы. DX многословнее, но «из коробки» закрыт весь путь от SQL до HTML витрины.

Главные правила, которые держат систему в порядке:

  • сначала решите уровень (данные / фрагмент / страница) — потом пишите ключ;
  • TTL без стратегии инвалидации подходит только там, где stale допустим;
  • в Laravel tags требуют совместимого store; в Bitrix не выдумывайте ORM_* теги;
  • Composite и любой page cache — это политика персонализации, а не только «галочка performance».
Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…
Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти