Система кеширования — одна из ключевых составляющих производительности платформы Melbis. Её философия проста: разработчик пишет модуль так, как удобно с точки зрения логики и читаемости кода, а платформа берёт на себя вопросы оптимизации за счёт нескольких уровней кеширования.
В Melbis Shop доступно пять видов кеша. Каждый решает свою задачу и может использоваться независимо или в сочетании с другими.
Кеширование на уровне отдельного 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. Отключив
кеширование на время разработки, вы отключаете и этот уровень тоже, и
видеть устаревшие справочные данные не будете.
Решает проблему запроса 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 не
вызван — и каждый вложенный некешированный модуль выполнял бы свой
отдельный запрос.
Платформа решает это автоматически, настраивать ничего не нужно:
EnumSet за время выполнения, записываются в
служебный заголовок файла кеша — первую строку вида
#MELBIS:{"enum":{...}} (JSON-метаданные; в HTML-вывод они
не попадают, но вы увидите эту строку, открыв файл кеша напрямую).SqlSelectEnumFlat в дочерних
модулях снова работает одним запросом, как при обычном запуске.Приятное следствие: восстановленный набор всегда консистентен с закешированным HTML — вложенные модули запросят ровно те ID, которые реально выведены в этой копии вёрстки, даже если это Trick-копия со слегка устаревшими данными.
Основной и наиболее универсальный вид кеширования. Включается для каждого модуля отдельно в IDE на вкладке «Параметры».
После выполнения модуля его HTML-результат сохраняется на диск. При следующем вызове с теми же входными параметрами парсер отдаёт уже готовый HTML, не запуская модуль заново.
Параметры базового кеша:
0 означает «без паузы»: кеш
обновляется сразу при изменении данных в отслеживаемых таблицах.
Ненулевое значение позволяет разгрузить сервер — кеш не перестраивается
чаще, чем раз в N минут, даже если данные изменились.Как платформа определяет, когда кеш устарел:
Каждый модуль указывает, с какими таблицами БД он работает (вкладка «Таблицы» в 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();Защитный механизм против пиковых нагрузок. Настраивается на вкладке «Trick» в IDE.
Суть: при высокой нагрузке на сервер или долгой компиляции страницы вместо того, чтобы генерировать страницу заново, платформа отдаёт более старую версию кеша — ту, что была сохранена ранее. Это позволяет сайту оставаться работоспособным даже при неожиданном всплеске трафика или попытке атаки — сервер не захлёбывается в очереди из медленно генерируемых страниц.
Параметры Trick-кеша:
0 — без ограничений.Trick-кеш работает параллельно с базовым: при нормальной нагрузке модуль работает штатно, при пиковой — отдаёт сохранённую Trick-версию.
Работает в связке с базовым кешем при ненулевой паузе обновления. Настраивается на вкладке «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-модуль очистки, чтобы записать размер в журнал до и после уборки и увидеть, работает ли она вообще.
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.