Руководство Melbis Shop: оглавление
Система кеширования — одна из ключевых составляющих производительности платформы 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 показывает собранный список, и разработчику остаётся одно действие — снять галку с таблицы, от которой кеш зависеть не должен. Обычно это журнал или счётчик: пишется часто, а на выдачу не влияет. Снятая галка держится вечно — переживает сохранение, и движок никогда не запишет эту таблицу обратно.
Сохранение файла сбрасывает кеш. Правило одно и на
код, и на вёрстку: сохранили .php модуля или его
.htm-шаблон — базовый и Trick-кеш этого модуля исчезли,
чистить руками нечего. Сохранение библиотеки снимает кеш со всех
модулей, которые её подключают.
Кеш при этом снимается по всем группам шаблонов, а
не только по той, где правился шаблон: разбираться, чей именно шаблон
сохранён, дороже одной лишней перерисовки. А вот вставка тега
{MELBIS:другой_модуль(...)} в чужой шаблон сбрасывает кеш
того модуля, чей шаблон сохранён, — вызываемый о правке не знает, и его
кеш при необходимости чистится отдельно.
Важно: таблица попадает в список после того, как модуль к ней обратился, поэтому первая отрисовка после сохранения идёт без кеша — движок в этот момент список и составляет. Со второго вызова кеш работает как обычно.
Важно: модуль, не зависящий ни от входных параметров, ни от таблиц, не кешируется вовсе — кешировать в нём нечего, и пустой список означает именно это.
Важно: Таблицы добавляются в список отслеживаемых только если они реально существуют в базе данных на момент старта парсера. Поэтому временные таблицы (
TEMPORARY TABLE), создаваемые внутри модуля, автоматически исключаются из отслеживания — специально исключать их вручную не нужно.
Важно: библиотеки нужно подключать через IDE (галочки в списке библиотек), а не PHP-функциями
require/includeи не методомMELBIS()->UnitInc(). Таблицы учтутся в любом случае — движок приписывает запрос тому модулю, который его выполнил, где бы этот запрос ни лежал. Но список подключений нужен для другого: по нему движок находит, чей кеш сбросить, когда сохраняется сама библиотека. Модуль, подключивший её в обход списка, останется со старым кешем.
Важно: Если модуль обращается к данным через
$_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-кеш обеспечит актуальность данных и сгладит нагрузку, не допуская лавинного обновления.
Разделяйте библиотеки по назначению. От всех таблиц библиотеки модуль не зависит — только от тех, к которым обратились вызванные им функции. Но сохранение библиотеки сбрасывает кеш всех, кто её подключает, так что одна библиотека на весь магазин означает полный пересчёт витрины после каждой её правки.
Используйте Trick для критичных блоков. Шапка сайта, каталог, главная страница — всё, что должно оставаться доступным при любой нагрузке, должно иметь Trick-кеш.
Планируйте очистку кеша. Настройте cron-модуль с
ежесуточным вызовом CacheBaseClear,
CacheTrickClear и CacheStaticClear.