В прошлой части мы разбирали события и асинхронность: что происходит после важного действия и в каком процессе это должно жить.
Этот слой — про то, как не делать одну и ту же дорогую работу дважды. Кэш — не «ускоритель в конце проекта», а архитектурный инструмент: он задаёт, какие данные считаются свежими, кто отвечает за инвалидацию и где проходит граница между «быстрым ответом» и «устаревшей правдой».
Разбор опирается на актуальную документацию 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 за то, чтобы не повторять дорогую операцию.
Три разных «объекта кэширования», которые часто путают:
- Данные — результат запроса, DTO, список ID. Инвалидируется при изменении источника.
- Фрагмент UI — HTML блока/компонента. Зависит от параметров, языка, групп пользователя.
- Страница целиком — полный 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.
Обычный 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.
- Fresh (0–300 сек) — кэш свежий, отдаём как есть, ничего не пересчитываем.
- Stale (300–600 сек) — формальный TTL вышел, но ещё «льготное» окно: пользователю отдаём старое значение сразу, а пересчёт уходит в deferred-функцию после ответа.
- Expired (после 600 сек) — льготное окно тоже кончилось: пересчитываем синхронно, пользователь ждёт.
Зачем: убрать latency-спайк у первого запроса после протухания.
Минус: короткое время можно отдать слегка устаревшие данные — для ленты/витрины часто ок, для остатков, цен и прав — нет.
Теги
Теги позволяют сбросить группу ключей одной операцией:
Cache::tags(['posts', 'homepage'])->remember('posts.latest', 3600, function () {
return Post::latest()->limit(20)->get();
});
// После обновления поста:
Cache::tags(['posts'])->flush();
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_TYPE—A/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→ ORMcache.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
- Store —
redis(илиmemcached): tags обязательны для этого упражнения. - Сервис
PostListService::latest(): Collectionвнутри используетCache::tags() - В модели/observer
Postнаsaved/deleted:flush() - Проверьте:
- первый запрос — miss + запись;
- второй — hit;
- после правки заголовка — список обновился без ожидания TTL;
- на
CACHE_STORE=databasetags недоступны (осознанно сломайте и верните Redis).
Документация: Cache, Cache Tags.
Bitrix
- Вынесите выборку в сервис модуля, оберните в
Cache+TaggedCacheсcacheDir = '/my_shop/posts'и тегомmy_shop_posts_list. - Зарегистрируйте handler на событие сохранения элемента/записи сущности:
clearByTag('my_shop_posts_list'). - Отдельно включите ORM-cache на
getListсttlи проверьте, что после update помогаетPostTable::cleanCache()— это другой слой, не замена TaggedCache для HTML. - Опционально: выведите список компонентом с
startResultCacheиCACHE_GROUPS=Y, сравните ключ для гостя и для админа. - Проверьте:
- 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».