Modx проблемы с кэшем

Обновлено: 04.07.2024

Кэшируя повторно используемые данные, можно предотвратить множество запросов к базе данных, что приведет к повышению производительности. MODX Revolution предлагает ряд различных функций кэширования на разных уровнях в приложении. Кэширование в MODX в основном обрабатывается базовым классом modCacheManager , который расширяет класс xPDOCacheManager и позволяет использовать обработчики кэша, зависящие от раздела. Реализация по умолчанию записывает кэши в файлы в папке core/cache/ .

Если вы определили пользовательский ключ MODX_CONFIG_KEY , менеджер кэша выполнит запись в core/cache/MODX_CONFIG_KEY/

Общая терминология кэширования и поведение¶

MODX использует разные разделы для отдельных типов кэшируемых данных. Упрощенно, раздел - это папка в директории core/cache/ , но настоящая ценность разделов в том, что каждому разделу могут быть назначены разные обработчики кэша. Обработчики кэша являются производными от класса xPDOCache и предоставляют единый API для хранения, чтения и удаления записей кэша.

Обработчик кэша по умолчанию xPDOFileCache записывает кэш в файловую систему в папке core/cache/ , но в ядре доступны также другие обработчики кэша для APC ( xPDOAPCCache ), memcache(d) ( xPDOMemCache , xPDOMemCached ) и WinCache ( xPDOWinCache ).

Разделы основного кэша MODX¶

В ядре несколько разделов. Их можно легко определить, просмотрев папку core/cache/ с конфигурацией кэша по умолчанию.

Обычно вам не нужно работать с кэшированными данными напрямую (вместо этого используйте доступные API), но для понимания ядра MODX здесь мы рассмотрим основные разделы и кратко опишем их назначение и содержание.

Как мы обсудим позже, пользовательские провайдеры также могут быть использованы в разработке.

  • action_map содержит большой массив всех действий (идентификаторов, ссылающихся на контроллеры и пространства имен), которые могут быть доступны в менеджере. Поскольку действия устарели и больше не используются в 2.3, никогда не полагайтесь на них.
  • auto_publish содержит метку времени Unix, которая определяет, когда ресурс должен быть автоматически опубликован или распубликован (см. ModCacheManager.autoPublish() )
  • context_settings для каждого контекста на сайте содержит карту ресурсов (идентификаторы родительских и дочерних документов), карту псевдонимов, используемые в контексте плагины и политики доступа.
  • db раздел кэша базы данных используется, когда включена системная или контекстная настройка cache_db , и содержит необработанные наборы результатов для запросов xPDO getObject / getCollection . Подробнее об этом ниже.
  • includes - на самом деле, это не раздел кэша, но он содержит файлы PHP, где сниппеты и плагины заключены в вызовы функций для легкого выполнения ядром. Смотрите сценарии для раздела кэша для сниппетов и плагинов.
  • logs - это также не раздел кэша, но содержит файл error.log и иногда другие файлы журнала (например, журнал установки).
  • menu cодержит для каждого языка менеджера многомерный массив верхнего меню менеджера.
  • mgr не является настоящим разделом кэша, но используется Smarty и Google Minify в 2.2 для записи файлов кэша.
  • registry - это директория по умолчанию для modRegistry, в которую записываются журналы регистрации файлов. Не является настоящим разделом кэша.
  • resource содержит организованный по контексту и идентификатору ресурса механизм частичного кэширования ресурсов. Эти файлы кэша содержат метаданные для ресурса, кэшированное представление ресурса (_content) с оставшимися без кэширования тегами, политиками доступа к ресурсу и элементами и их источниками, используемые при обработке ресурса.
  • rss не является настоящим разделом кэша, но используется MagpieRSS (виджеты RSS панели) для записи в кеш.
  • scripts содержит источник сниппетов и плагинов, которые впоследствии записываются в папку кэша includes.
  • setup не является настоящим разделом кэша, но используется инсталлятором MODX для кэширования шаблонов Smarty.
  • system_settings содержит глобальную конфигурацию MODX и системные настройки. Этот раздел загружается первым по запросам в MODX. Поскольку альтернативные обработчики кэша для разделов хранятся в системных настройках, этот раздел не может быть загружен из другого обработчика кэша таким образом.

Чтобы изменить обработчик кэша для определенного раздела кэша, просто создайте новый системный (или контекстный) параметр с именем cache_PARTITION_handler (например, cache_resource_handler или cache_scripts_handler ) и присвойте ему значение обработчика кэша, который вы хотели бы использовать. По умолчанию используется xPDOFileCache , однако и другие обработчики доступны для APC , memcache (d) и wincache .

Обратите внимание, что в MODX 2.0.x система кэша довольно сильно отличалась. Доступные разделы были иными, а системные настройки сохранялись в core/cache/config.cache.php . Если вы все еще используете MODX 2.0.x, вам следует потратить больше времени на обновление и меньше времени на чтение этого документа.

Кэширование базы данных¶

Если вы включите системный параметр cache_db, MODX может автоматически кэшировать наборы результатов базы данных, извлеченные любым экземпляром xPDOCriteria или xPDOQuery . Это включает в себя все наборы результатов, представляющие xPDOObjects или коллекции xPDOObjects , возвращаемые такими методами, как getObject и getCollection .

Эта функция может быть включена в средах, где доступ к базе данных обходится дороже, чем время подключения файлов PHP, например, при использовании внешнего сервера базы данных, или настраивается для сред с доступным memcached , APC или другими системами кэширования. Это отдельный раздел кеша в MODX, поэтому его можно настроить с другими обработчиками кэша. Смотрите xPDO Caching для дополнительной информации.

Обновление кэша MODX Core¶

Чтобы обновить любой из основных разделов кэша MODX, используйте метод modCacheManager->refresh() . Минимальный вызов не имеет параметров и обновит все разделы основного кэша.

Кроме того, вы можете определить массив $providers с разделом элементов key => $partitionOptions .

Второй параметр $results передается по ссылке и будет содержать результаты каждого раздела кэша. В зависимости от раздела это может быть логическое значение или массив с дополнительной информацией о результате обновления определенного раздела. Сама функция возвращает логическое значение, указывающее, вернул ли какой-либо из разделов логическое значение false .

Программное (пользовательское) кэширование¶

Взаимодействуя с modCacheManager , вы можете легко кэшировать данные любого типа. Есть несколько полезных функций, которые вы можете использовать для поддержания рабочего кэша. Используя modCacheManager с пользовательским разделом (хотя и необязательно), пользователи вашего кода могут изменить обработчик кэша и сохранить данные в экземпляре memcached , APC или WinCache вместо файлового кэша по умолчанию.

ModCacheManager (производный от xPDOCacheManager ) предоставляет следующие полезные методы:

  • add($key, $var, $life = 0, $options = array()) используется для добавления значения в кэш, но только если оно еще не существует или срок его действия истек.
  • replace($key, $var, $life = 0, $options = array()) используется для замены существующего кэшированного значения другим.
  • set($key, $var, $life = 0, $options = array()) используется для установки значения в кэш независимо от того, существует ли оно уже (перезаписывается) или нет (добавляется).
  • delete($key, $options = array()) удаляет кэшированное значение из кэша.
  • get($key, $options = array()) получает кэшированное значение из кэша.
  • clean($options = array()) очищает (удаляет) весь поставщик кэша. Убедитесь, что вы определили xPDO::OPT_CACHE_KEY в массиве параметров.

В общем случае вы можете использовать get($key) и set($key, $value) для получения и установки значений соответственно, но дополнительные методы обеспечивают дополнительный контроль над способом управления данными.

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

  • xPDO::OPT_CACHE_KEY - раздел кэша для записи.
  • xPDO::OPT_CACHE_HANDLER - используемый обработчик кэша. Как правило, вам не нужно жестко определять этот параметр, но имеет смысл разрешать конкретной реализации обрабатывать обработчик кэша с помощью системных настроек (то есть системных настроек cache_PARTITION_handler ).
  • xPDO::OPT_CACHE_EXPIRES - время истечения по умолчанию.

Пример 1: Простое добавление и получение кэша¶

Пример 2. Добавление и получение кэша из пользовательского раздела¶

Обратите внимание в Revolution 2.0¶

В MODX Revolution 2.0 была другая система кеширования с отличающимися разделами. Чтобы очистить кеш в 2.0, вы должны использовать метод clearCache() , который устарел с 2.1. Лучше обновиться до последней версии, чем продолжать использовать 2.0.

Разработано, построено и написано со всей любовью в мире от сообщества MODX.

Многие разработчики сталкивались с проблемой кэширования при использовании шаблонизатора Fenom. И я в том числе. Эта проблема не раз поднималась в сообществе — раз, два, три и ещё много комментариев в других постах. Об этом писал даже Василий (автор pdoTools). Вот анализ проблемы Николая Ланца. Но решение так и не было найдено, хотя попытки были. Многие просто отключали кэш ресурсов или отказывались от кэширования сниппетов. Кто-то правил исходники проблемных пакетов. А так как эта хрень с кэшем меня достала (постоянно отваливается редактор markItUp для комментариев), то я решил таки потратить пару часов и вникнуть, почему это происходит и как это решить.

Налил себе большой бокал чая и начал погружаться. В итоге ситуация оказалась ровно такой, как и написал Николай. Но и он не стал до конца вникать в проблему (оно и понятно, он использует Smarty). И поэтому решение, которое он предложил, не совсем корректное. Он оптимизировал задачу, которую описал во втором абзаце своего комментария. Я полностью согласен с его выводом. Но проблему с кэшированием, ссылки на которую я давал в самом начале, это не решает. Сразу скажу, что эта проблема напрямую никак не связана с Fenom. Просто вот эти строчки в парсере pdoParser нарушают логику кэширования MODX.

Лично у меня два раза выводился скрипт с настройками TicketConfig. И получалось, что второй скрипт перебивал первый, заново переопределял переменную TicketConfig и затирал настройки редактора markItUp.

Опущу рассказ о заваривании второй чашки и дальнейшем погружении в логику кэширования и перейду сразу к её описанию. В MODX контент ресурса кэшируется отдельно от скриптов и стилей. Т.е. кэшируется не готовая страница, а массив параметров, в котором отдельно указан контент, стили, скрипты, кэшируемые элементы и ещё куча других параметров. Так вот, логика следующая — если на странице вызывается кэшированный сниппет, в котором подключается скрипт или стили, то эти файлы сохраняются в массиве в отдельных ключиках — _sjscripts , _jscripts и _loadedjscripts . Т.е. в этом случае эти файлы сохраняются в ресурсе:

А если сниппет некэшированный, то в ресурс ничего сохраняться не будет. Это логично. Сниппет выполнится и сам подключит файлы. А вот если он вызывается без знака ! , то выполнится он только первый раз, и парсер заменит его плейсхолдер на результат его работы, который попадёт в кэш ресурса. Но так как этот сниппет подключал файлы, то MODX их указывает в соответствующих ключах массива закэшированного ресурса. При обращении к ресурсу, MODX будет искать файл кэша, получит контент и динамически подключит указанные файлы. Наверно, было бы проще кэшировать страницу уже с подключёнными скриптами и стилями. Но разработчики MODX таким образом борются с дублями, чтобы один и тот же скрипт не подключался 2 раза. Им видней, но мне кажется эту задачу нужно переложить на разработчиков.

Для информации!

Свойство класса modX sjscripts используется для хранения скриптов, стилей и HTML кода, которые должны подключаться в секции head HTML страницы. Скрипты и стили, указанные в свойстве jscripts , вставляются перед закрывающим тегом body . А в loadedjscripts указываются все скрипты, стили и HTML блоки, подключенные на странице. Последнее свойство используется только для логирования уже подключённых скриптов — перед подключением нового скрипта MODX проверяет, нет ли такого в списке.

Таким образом, если коротко, то процесс подготовки контента выглядит так:

Как видите, в кэш ресурса сохраняются файлы только для кэшированных сниппетов. А если мы посмотрим в pdoParser, то увидим, что подключенные скрипты и стили сохраняются в кэше ресурса и для кэшированных и для некэшированных сниппетов и на 3-ем шаге и на 4-ом, когда, согласно логике MODX, их сохранять уже нельзя. Николай предлагает сохранить в кэш все скрипты/стили (и из кэша ресурса и подключённые) только один раз на событие OnBeforeSaveWebPageCache на 6-ом шаге. Но проблема остаётся — в кэш попадают скрипты, которые туда попадать не должны.

Вообще, Василий об этой проблеме писал в статье про pdoTools 2.7.0.

Но это решение только для регистрации скриптов через феном и для обычной регистрации оно не работает. Как видите, задача нетривиальная и ещё есть над чем подумать.

Update 06.10.2017. Свой вариант решения этой проблемы отправил Василию (автору pdoTools), так как заинтересован, чтобы всё работало из коробки.

Update 22.10.2017. Мой PR принят. Теперь о проблеме с кэшированием сниппетов можно забыть.

Если Вы сделали несколько сайтов на Modx, то скорее всего уже сталкивались с проблемой кэширования и корректной очистки кэша. Ниже показывается, как работает кэш Modx и как создать плагин для очистки кэша одного ресурса.

Файловый кэш по умолчанию в Modx Revo работает не всегда так, как нам хочется. Что именно не так?

За кэширование документов в Modx отвечают 2 галочки на вкладке Настройки. Если отмечена галочка "Кэшируемый", а она отмечена по умолчанию, то при первом обращении к документу для него сохранится кэш в файле, из которого чтение будет происходить заметно быстрее. Если, конечно, у Вас не статичная страничка с чистым html. Отметив галочку "Очистить кэш", мы задаем принудительную очистку для того, чтобы при следующем обращении к ресурсу кэш пересоздался заново, и посетители сайта увидели сделанные нами изменения.

Проблема заключается в том, что при сохранении документа очищается весь кэш на сайте, а не того документа, изменения в котором мы делали. Если у нас небольшой сайт-визитка с малым временем генерации страниц, то на работу сайта это критично не скажется. Но если у нас ресурс побольше с несколькими сотнями тяжелых страниц, генерация которых занимает по 2-3 секунды, то гроханье всего кэша при правке небольшой опечатке в одной статье становится большой роскошью.

Все, что нам нужно, это создать плагин для очистки кэша конкретного ресурса при сохранении документа. При этом кэш других документов должен оставаться нетронутым. Решение подсмотрено у Василия Безумкина и немного расширено под наши нужды.

Итак, сам плагин. Нужно создать его под любым именем и назначить ему системные события OnDocFormRender и OnDocFormSave. На первом событии мы отключаем параметр "Очистить кэш". Можете убедиться в этом, включив плагин и открыв в админке любой документ - галочка будет снята. Второе событие вызывает код, очищающий кэш текущего документа.

Все работает отлично! Но теперь давайте посмотрим чуть дальше.

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

Конечно много уже кто бросал камни в MODX из-за проблем с кешированием, но сегодня сделаю это и я. Я конечно же очень люблю MODX, но некоторые вещи меня прямо-таки вымораживают! Сразу оговорюсь, что описываемые здесь проблемы касаются только тех случаев, когда предполагается большое количество документов в одном контексте (более 10 000).

Сегодня мы рассмотрим процесс генерации кеша контекстов и на что и как мы можем влиять.

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

А в чем проблема? А проблема в том, что это как минимум накладывает очень серьезные ограничения на максимальное кол-во документов в контексте. Почти два года назад я уже писал о своих исследованиях по этому поводу еще на версии Revo 2.0.8, так вот — с тех пор практически ничего не поменялось…

Сразу определим основную проблему: при обновлении кеша контекста, MODX перебирает все документы этого контекста (читай: делает много-много запросов к базе данных и получает и обрабатывает очень большой объем информации) и формирует карты ресурсов и алиасов. При этом он хранит эти карты не в отдельном кеш-файле, а именно в кеше настроек контекста.

Есть проблема — сразу же можно предположить парочку вариантов ее решения: 1. Запретить MODX-у делать выборку всех документов контекста. (Это был бы идеальный вариант — частично закешировать только важные документы, участвующие в формировании менюшек Wayfinder-ом и т.п., а те документы, которые мы получаем динамически нашими собственными специфическими скриптами, пропустить). 2. Вообще отключить кеширование контекста. (Почему это оказывается очень плохой вариант, мы рассмотрим и поймем позже).

Для начала немного теории: каждый раз при генерации настроек контекста, MODX собирает не только его настройки как таковые, но и собирает все его документы и набивает в карты ресурсов, алиасов и т.п. Плюс к этому, если используются ЧПУ, он еще и проверяет их на уникальность.

Выполняется это все в одном методе modCacheManager::generateContext(). Давайте посмотрим на исходник:

<?php public function generateContext ( $key , array $options = array ( ) ) < $results = array ( ) ; if ( ! $this ->getOption ( 'transient_context' , $options , false ) ) < /** @var modContext $obj */ $obj = $this ->modx -> getObject ( 'modContext' , $key , true ) ; if ( is_object ( $obj ) && $obj instanceof modContext && $obj -> get ( 'key' ) ) < $cacheKey = $obj ->getCacheKey ( ) ; $contextKey = is_object ( $this -> modx -> context ) ? $this -> modx -> context -> get ( 'key' ) : $key ; $contextConfig = array_merge ( $this -> modx -> _systemConfig , $options ) ; /* generate the ContextSettings */ $results [ 'config' ] = array ( ) ; if ( $settings = $obj -> getMany ( 'ContextSettings' ) ) < /** @var modContextSetting $setting */ foreach ( $settings as $setting ) < $k = $setting ->get ( 'key' ) ; $v = $setting -> get ( 'value' ) ; $matches = array ( ) ; if ( preg_match_all ( '

' , $v , $matches , PREG_SET_ORDER ) ) < foreach ( $matches as $match ) < if ( array_key_exists ( " < $match [ 1 ] >" , $contextConfig ) ) < $matchValue = $contextConfig [ " < $match [ 1 ] >" ] ; > else < $matchValue = '' ; >$v = str_replace ( $match [ 0 ] , $matchValue , $v ) ; > > $results [ 'config' ] [ $k ] = $v ; $contextConfig [ $k ] = $v ; > > $results [ 'config' ] = array_merge ( $results [ 'config' ] , $options ) ; /* generate the aliasMap and resourceMap */ $collResources = $obj -> getResourceCacheMap ( ) ; $results [ 'resourceMap' ] = array ( ) ; $results [ 'aliasMap' ] = array ( ) ; if ( $collResources ) < /** @var Object $r */ while ( $r = $collResources ->fetch ( PDO :: FETCH_OBJ ) ) < $results [ 'resourceMap' ] [ ( string ) $r ->parent ] [ ] = ( string ) $r -> id ; if ( $this -> modx -> getOption ( 'friendly_urls' , $contextConfig , false ) ) < if ( array_key_exists ( $r ->uri , $results [ 'aliasMap' ] ) ) < $this ->modx -> log ( xPDO :: LOG_LEVEL_ERROR , "Resource URI < $r ->uri > already exists for resource > < $results [ 'aliasMap' ] [ $r ->uri ] > ; skipping duplicate resource URI for resource > < $r ->id > " ) ; continue ; > $results [ 'aliasMap' ] [ $r -> uri ] = $r -> id ; > > > /* generate the webLinkMap */ $collWebLinks = $obj -> getWebLinkCacheMap ( ) ; $results [ 'webLinkMap' ] = array ( ) ; if ( $collWebLinks ) < while ( $wl = $collWebLinks ->fetch ( PDO :: FETCH_OBJ ) ) < $results [ 'webLinkMap' ] [ $wl ->id ] = $wl -> content ; > > $this -> modx -> log ( modX :: LOG_LEVEL_ERROR , $key ) ; /* generate the eventMap and pluginCache */ $results [ 'eventMap' ] = array ( ) ; $results [ 'pluginCache' ] = array ( ) ; $eventMap = $this -> modx -> getEventMap ( $obj -> get ( 'key' ) ) ; if ( is_array ( $eventMap ) && ! empty ( $eventMap ) ) < $results [ 'eventMap' ] = $eventMap ; $pluginIds = array ( ) ; $plugins = array ( ) ; $this ->modx -> loadClass ( 'modScript' ) ; foreach ( $eventMap as $pluginKeys ) < foreach ( $pluginKeys as $pluginKey ) < if ( isset ( $pluginIds [ $pluginKey ] ) ) < continue ; >$pluginIds [ $pluginKey ] = $pluginKey ; > > if ( ! empty ( $pluginIds ) ) < $pluginQuery = $this ->modx -> newQuery ( 'modPlugin' , array ( 'id:IN' => array_keys ( $pluginIds ) ) , true ) ; $pluginQuery -> select ( $this -> modx -> getSelectColumns ( 'modPlugin' , 'modPlugin' ) ) ; if ( $pluginQuery -> prepare ( ) && $pluginQuery -> stmt -> execute ( ) ) < $plugins = $pluginQuery ->stmt -> fetchAll ( PDO :: FETCH_ASSOC ) ; > > if ( ! empty ( $plugins ) ) < foreach ( $plugins as $plugin ) < $results [ 'pluginCache' ] [ ( string ) $plugin [ 'id' ] ] = $plugin ; >> > /* cache the Context ACL policies */ $results [ 'policies' ] = $obj -> findPolicy ( $contextKey ) ; > > else < $results = $this ->getOption ( " < $key >_results" , $options , array ( ) ) ; $cacheKey = " < $key >/context" ; $options [ 'cache_context_settings' ] = array_key_exists ( 'cache_context_settings' , $results ) ? ( boolean ) $results : false ; > if ( $this -> getOption ( 'cache_context_settings' , $options , true ) && is_array ( $results ) && ! empty ( $results ) ) < $options [ xPDO :: OPT_CACHE_KEY ] = $this ->getOption ( 'cache_context_settings_key' , $options , 'context_settings' ) ; $options [ xPDO :: OPT_CACHE_HANDLER ] = $this -> getOption ( 'cache_context_settings_handler' , $options , $this -> getOption ( xPDO :: OPT_CACHE_HANDLER , $options ) ) ; $options [ xPDO :: OPT_CACHE_FORMAT ] = ( integer ) $this -> getOption ( 'cache_context_settings_format' , $options , $this -> getOption ( xPDO :: OPT_CACHE_FORMAT , $options , xPDOCacheManager :: CACHE_PHP ) ) ; $options [ xPDO :: OPT_CACHE_ATTEMPTS ] = ( integer ) $this -> getOption ( 'cache_context_settings_attempts' , $options , $this -> getOption ( xPDO :: OPT_CACHE_ATTEMPTS , $options , 10 ) ) ; $options [ xPDO :: OPT_CACHE_ATTEMPT_DELAY ] = ( integer ) $this -> getOption ( 'cache_context_settings_attempt_delay' , $options , $this -> getOption ( xPDO :: OPT_CACHE_ATTEMPT_DELAY , $options , 1000 ) ) ; $lifetime = ( integer ) $this -> getOption ( 'cache_context_settings_expires' , $options , $this -> getOption ( xPDO :: OPT_CACHE_EXPIRES , $options , 0 ) ) ; if ( ! $this -> set ( $cacheKey , $results , $lifetime , $options ) ) < $this ->modx -> log ( modX :: LOG_LEVEL_ERROR , 'Could not cache context settings for ' . $key . '.' ) ; > > return $results ; >

Первое, на что сразу следует обратить внимание — откуда происходит выборка настроек, к примеру вот здесь:

$this -> getOption ( 'transient_context' , $options , false )

$this — это не объект контекста, а сам modCacheManager, то есть выборка настроек происходит не из настроек контекста, а из переменной $options, переданной в метод generateContext. Посмотрим, какой параметр передается сюда при генерации кеша контекстов:

Читайте также: