Кеширование

Система кеширования — одна из ключевых составляющих производительности платформы Melbis. Её философия проста: разработчик пишет модуль так, как удобно с точки зрения логики и читаемости кода, а платформа берёт на себя вопросы оптимизации за счёт нескольких уровней кеширования.

В Melbis Shop доступно пять видов кеша. Каждый решает свою задачу и может использоваться независимо или в сочетании с другими.


1. Статический кеш запросов (APCu)

Кеширование на уровне отдельного SQL-запроса. Результат сохраняется в оперативной памяти сервера через расширение APCu. При повторном вызове с теми же параметрами запрос к БД не выполняется — результат берётся из памяти.

По умолчанию срок хранения — сутки, при необходимости можно задать больше. Но при этом кеш в любом случае станет невалидным, если в задействованных таблицах появились изменения: парсер автоматически находит все таблицы в тексте запроса (по шаблону {DBNICK}_...) и привязывает кеш ко времени их последнего изменения.

Удобно для часто вызываемых справочных данных внутри модулей, где нет смысла включать базовый кеш всего модуля, но повторяющиеся однотипные запросы создают ненужную нагрузку.

// Загрузить список валют — одинаковый для всей страницы
$currencies = MELBIS()->SqlSelectStatic(__LINE__,
    "SELECT id, name, rate
       FROM {DBNICK}_currency
      WHERE active = 1"
    // таймаут не указан — используется значение по умолчанию (сутки)
);

// Загрузить одну запись
$settings = MELBIS()->SqlSelectStaticFlat(__LINE__,
    "SELECT *
       FROM {DBNICK}_key_value
      WHERE key_name = :KEY",
    ['key' => 'delivery_free_limit']
);

SqlSelectStatic возвращает массив строк, SqlSelectStaticFlat — одну плоскую запись, SqlSelectStaticValue — одно значение (см. «Работа с базой данных»).

Оба метода подчиняются общему выключателю MELBIS_CACHE из config.json: при false они не читают и не пишут APCu, а просто выполняют запрос — как обычные SqlSelect и SqlSelectFlat. Отключив кеширование на время разработки, вы отключаете и этот уровень тоже, и видеть устаревшие справочные данные не будете.


2. Кеш наборов данных (Enum)

Решает проблему запроса N+1 — когда в цикле по списку товаров для каждого элемента делается отдельный запрос (например, за изображением или характеристиками).

Принцип работы. В модуле, который собирает список товаров, заранее регистрируется набор идентификаторов через EnumSet. Вложенный модуль при обращении к SqlSelectEnumFlat получает через EnumGet сформированный список ID для подстановки в запрос — и загружает все нужные данные одним запросом. При последующих вызовах данные берутся из памяти.

// В модуле списка товаров:
$goods = MELBIS()->SqlSelect(__LINE__, "SELECT id, name, price FROM {DBNICK}_store WHERE ...");

// Зарегистрировать набор ID
MELBIS()->EnumSet('store', array_column($goods, 'id'));

Вложенный модуль melbis_store_image:

$image = MELBIS()->SqlSelectEnumFlat(__LINE__,
    "SELECT store_id, file_name, upload_time
       FROM {DBNICK}_store_image
      WHERE store_id IN (:IDS)
        AND kind_key = :KIND",
    'store_id',
    $id,
    ['kind' => 'kDefault']
);

Метод EnumGet внутри SqlSelectEnumFlat автоматически формирует безопасный список идентификаторов для подстановки в IN (...) без использования подготовленных запросов — значения приводятся к целым числам и объединяются через запятую.

EnumSet и EnumGet поддерживают несколько массивов для одного ключа. Это особенно важно на страницах, где одни и те же объекты появляются в разных блоках. Например, на странице каталога могут быть три разных списка товаров: основной раздел, хиты продаж, новинки. Для каждого делается свой EnumSet:

MELBIS()->EnumSet('store', array_column($goods_main,     'id'));
MELBIS()->EnumSet('store', array_column($goods_hits,     'id'));
MELBIS()->EnumSet('store', array_column($goods_new,      'id'));

Когда вложенный модуль вызывает SqlSelectEnumFlat с конкретным $id, метод EnumGet перебирает все зарегистрированные наборы под именем 'store' и находит тот, в котором есть этот идентификатор. Запрос с соответствующим набором ID окажется уже закешированным — данные возьмутся из памяти. Если товар не попал ни в один набор — EnumGet вернёт набор из одного текущего элемента, и запрос выполнится как обычно.

Enum и базовый кеш модуля

У связки Enum + базовый кеш есть неочевидный момент: наборы регистрирует PHP-код модуля-списка, но когда этот модуль отдаётся из кеша, его PHP-код не выполняется. Без специальной поддержки это возвращало бы проблему N+1: родитель из кеша, EnumSet не вызван — и каждый вложенный некешированный модуль выполнял бы свой отдельный запрос.

Платформа решает это автоматически, настраивать ничего не нужно:

Приятное следствие: восстановленный набор всегда консистентен с закешированным HTML — вложенные модули запросят ровно те ID, которые реально выведены в этой копии вёрстки, даже если это Trick-копия со слегка устаревшими данными.


3. Базовый кеш модуля

Основной и наиболее универсальный вид кеширования. Включается для каждого модуля отдельно в IDE на вкладке «Параметры».

После выполнения модуля его HTML-результат сохраняется на диск. При следующем вызове с теми же входными параметрами парсер отдаёт уже готовый HTML, не запуская модуль заново.

Параметры базового кеша:

Как платформа определяет, когда кеш устарел:

Каждый модуль указывает, с какими таблицами БД он работает (вкладка «Таблицы» в IDE). Парсер при запуске загружает время последнего изменения всех таблиц и отслеживает его. При обновлении таблицы кеш зависящих от неё модулей автоматически становится устаревшим.

Важно: Таблицы добавляются в список отслеживаемых только если они реально существуют в базе данных на момент старта парсера. Поэтому временные таблицы (TEMPORARY TABLE), создаваемые внутри модуля, автоматически исключаются из отслеживания — специально исключать их вручную не нужно.

Важно: Для корректной работы кеша нельзя подключать библиотеки внутри модуля ни PHP-функциями require/include, ни методом MELBIS()->UnitInc() — ни один из них не регистрирует таблицы библиотеки. Все зависимости должны быть подключены через IDE (галочки в списке библиотек). Только в этом случае парсер автоматически учтёт таблицы из подключённых библиотек при определении времени жизни кеша.

Важно: Если модуль обращается к данным через $_GET или $_POST напрямую (минуя входные параметры), при включённом кеше он будет работать некорректно — результат закешируется один раз и не будет учитывать разные значения этих данных. Все входные данные должны передаваться как объявленные параметры модуля.

По той же причине не стоит читать глобальный массив через GlobalFetch внутри кода кешируемого модуля и вручную вставлять прочитанное в HTML — значение «запечётся» в кеш в момент первой генерации и не будет обновляться для других посетителей. Если кешируемый блок должен отображать живые данные из глобального массива (текущий язык, статус авторизации и т.п.) — используйте в его .htm-шаблоне обычный тег (например, {VAR:LANG}), а не GlobalFetch в PHP: такие теги разворачиваются на отдельном проходе, который никогда не кешируется — подробнее в разделе «Глобальный массив» → «Пробиваем кэш модуля».

Не следует использовать PHP-функцию header() внутри модуля, результат которого кешируется: header() выводит данные непосредственно в поток и кешированию не подлежит. Заголовки следует устанавливать в корневом скрипте.

Наборы Enum, зарегистрированные модулем через EnumSet, автоматически сохраняются вместе с кешем и восстанавливаются при его чтении — вложенным модулям ничего специально делать не нужно. Подробнее — в разделе «Кеш наборов данных (Enum)» выше.

Принудительно сбросить кеш изнутри модуля (например, если что-то пошло не так и нужен повторный запуск):

MELBIS()->UnitCacheReset();

4. Trick-кеш (хитрый кеш)

Защитный механизм против пиковых нагрузок. Настраивается на вкладке «Trick» в IDE.

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

Параметры Trick-кеша:

Trick-кеш работает параллельно с базовым: при нормальной нагрузке модуль работает штатно, при пиковой — отдаёт сохранённую Trick-версию.


5. Smart-кеш (умный кеш)

Работает в связке с базовым кешем при ненулевой паузе обновления. Настраивается на вкладке «Smart» в IDE.

Проблема паузы кеша: когда задан интервал обновления, все копии кеша устаревают одновременно и в момент их сброса сервер получает резкий всплеск нагрузки — множество параллельных запросов разом перестраивают кеш. Плюс данные могут быть неактуальны бо́льшую часть времени паузы.

Решение — Smart-кеш: платформа анализирует текущую нагрузку и время компиляции и заблаговременно, в спокойный момент, обновляет кеш досрочно. В результате данные актуальнее, а нагрузка сглаживается.

Параметры Smart-кеша:

Важно: для работы Smart-кеша пауза базового кеша должна быть больше нуля.


Очистка кеша

Со временем на диске и в памяти накапливаются устаревшие файлы кеша. Платформа предоставляет инструменты для их очистки.

Автоматическая очистка при обновлении включается вызовом:

MELBIS()->CacheClearAuto();

При каждом обновлении базового кеша модуля старые файлы будут удаляться автоматически. Однако это не рекомендуется при высокой нагрузке — дополнительные операции с файловой системой увеличивают время генерации страницы.

Рекомендуемый подход — отдельный cron-модуль, который запускается раз в сутки и выполняет плановую очистку:

// В cron-модуле очистки кеша:
function MELBIS_CRON_CACHE_CLEAR($mVars)
{
    MELBIS()->CacheBaseClear();     // очистка базового кеша
    MELBIS()->CacheTrickClear();    // очистка Trick-кеша (удаляет файлы старше «Макс давность кеша»)
    MELBIS()->CacheStaticClear();   // очистка устаревших APCu-записей статического кеша

    return '';
}

Для работы CacheTrickClear у модулей должен быть задан параметр «Макс давность кеша» — именно по нему определяется, какие Trick-файлы считать устаревшими.

Контроль объёма

Два метода возвращают размер дисковых кешей в байтах — базового и Trick:

$base = MELBIS()->CacheBaseSize();
$trick = MELBIS()->CacheTrickSize();

Обход каталога — операция не бесплатная, поэтому вызывать их со страниц витрины не стоит. Их место — служебная панель или тот же cron-модуль очистки, чтобы записать размер в журнал до и после уборки и увидеть, работает ли она вообще.

Сброс статистики Smart-кеша

CacheSmartReset($mUnitName = false) очищает накопленные Smart-кешем данные о нагрузке и времени компиляции: с аргументом — по одному модулю, без аргумента — целиком.

// Забыть статистику одного модуля
MELBIS()->CacheSmartReset('melbis_store_topic');

// Сбросить всю накопленную статистику
MELBIS()->CacheSmartReset();

Нужен после существенной переделки модуля: Smart-кеш решает, когда обновляться досрочно, опираясь на прошлые замеры, и если модуль стал вдвое быстрее или медленнее, старая статистика будет уводить решения не туда. Тот же сброс делается из IDE кнопкой «Сброс Smart-данных модуля» на вкладке «Smart». Возвращает false, если статистики по указанному модулю не было.


Пакетный режим данных и организация работы

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

Тем не менее организация работы имеет значение. Если менеджер будет сохранять каждый товар по одному каждые пять минут — кеш соответствующих модулей будет перестраиваться так же часто.

Для управления этим в программе Melbis Shop предусмотрены ограничения на выполнение операций: в разделе «Пользователи» для каждой операции можно задать как временны́е ограничения (например, запретить сохранение в час пик), так и ограничения по текущей нагрузке на сервер — операция разрешается только если сервер достаточно свободен.


Сводная таблица

Вид кеша Где хранится Управляется Назначение
SqlSelectStatic APCu (память) В коде модуля Кеш отдельных запросов без кеша всего модуля
Enum PHP-память (static) В коде модуля Устранение N+1 запросов в циклах
Базовый Диск IDE → Параметры Кеш результата целого модуля
Trick Диск IDE → Trick Защита от пиковых нагрузок
Smart APCu + диск IDE → Smart Сглаживание нагрузки при паузе кеша

Рекомендации

Не кешируйте всё подряд. Модули с уникальными входными параметрами при каждом вызове и малым временем выполнения кешировать нет смысла.

Задавайте большую паузу и включайте Smart-кеш. Если используется Smart, можно смело ставить паузу 100 минут и больше — Smart-кеш обеспечит актуальность данных и сгладит нагрузку, не допуская лавинного обновления.

Разделяйте библиотеки по назначению. Одна большая inc-библиотека, подключённая ко всем модулям, приведёт к тому, что изменение любой из её таблиц сбросит кеш всего сайта.

Используйте Trick для критичных блоков. Шапка сайта, каталог, главная страница — всё, что должно оставаться доступным при любой нагрузке, должно иметь Trick-кеш.

Планируйте очистку кеша. Настройте cron-модуль с ежесуточным вызовом CacheBaseClear, CacheTrickClear и CacheStaticClear.