Pull to refresh

CacheAccelerator для MODx Evo. Уменьшение в разы количества запросов к базе за счет кэширования динамических сниппетов

Reading time 9 min
Views 9.2K
Всем привет. Я совсем недавно познакомился с MODx CMF. Осваиваю в данный момент версию Evolution. Система в целом довольно приятная и очень гибкая, однако, ознакомившись поближе, я обнаружил ряд недостатков. Причем некоторые из них не давали мне никакого покоя и оставлять как есть я никак не смог.

Остановлюсь на одном из самых чувствительных критериев любой CMS/CMF — производительности.
В целом, с производительностью у MODx все норм. Сам он написан достаточно грамотно, оптимизирован. Более того, за счет своей гибкости, дает разработчику возможность самому управлять узкими местами в реализуемом проекте.

Тем не менее, меня просто шокировал метод обработки вывода новостей с помощью Ditto, комментариев с помощью Jot и тд. А именно, необходимость отключать кэширование как для всей страницы у Ditto (из-за проблем в работе с PHx), так и для вызова самого сниппета у Jot.

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

Что же советуют официальные источники?
Они советуют, чтобы сниппеты, работающие с несколькими
страницами, никогда не кэшировались.

Вот примеры вызова Ditto и Jot:

Страница вызова сниппета не кэшируется «Настройка страницы => Кэшируемый => Выкл»
[[Ditto?
&parents=`1`
&display=`10`
&paginate=`1`
&paginateAlwaysShowLinks=`1`
]]


Страница вызова сниппета кэшируется «Настройка страницы => Кэшируемый => Вкл»
[!Jot?
&customfields=`name,email`
&pagination=`10`
&badwords=`*****`
&canmoderate=`Site Admins`
&captcha=`1`!]
!]


Как мы видим, кэширование средствами MODx в обоих случаях не производится. Отсутствие кэширования напрямую сказывается на скорости работы системы, т.к. при каждом вызове страницы, все данные сниппета собираются из базы.

По моим тестам, страница вызова Ditto с 10-ю записями на странице, отображением заголовка, краткой аннотации и даты публикации, создает порядка 11-ти запросов к базе. Использование дополнительных фильтров, условий поиска по полям TV, даст еще более удручающую статистику.
image
демо

То же самое происходит и на странице вызова Jot.
Вызов Jot с 10-ю комментариями на страницу, стандартной информацией о каждом
комментарии и с формой для добавления новых сообщений, создает также порядка
22 — 24-х запросов к базе.
image
демо

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

Немного подумав, я написал CacheAccelerator, состоящий из одного модуля, одного сниппета и сторонней библиотеки.

Cache Accelerator, за счет кэширования выдачи любого произвольного сниппета (не только Ditto и Jot), увеличивает скорость работы системы и уменьшает количество запросов к базе.

По проведенным тестам, повторный запрос к рассмотренной выше странице с Ditto, дает результат в виде уменьшения количества обращений к базе с 11 до 3(!), а к странице с Jot, с 22 до 1(!).
imageimage

Данный продукт не использует никаких хак методов интеграции в движок MODx. Является совершенно отдельным блоком кода. Очень прост в установке и использовании.

Итак, установка:

Сперва скачиваем fileCache со страницы:
http://neo22s.com/filecache/

либо прямая ссылка:
http://lab.neo22s.com/fileCache/fileCache.zip

Создаем директорию /assets/plugins/cacheaccelerator
В директории cacheaccelerator создаем директорию cache (/assets/plugins/cacheaccelerator/cache)
На обе созданные директории устанавливаем chmod 777

Далее, из скачанного архива копируем файл fileCache.php в директорию /assets/plugins/cacheaccelerator

После этого, в менеджере MODx нажимаем Элементы -> Управление элементами -> Сниппеты -> Новый сниппет. Создаем новый сниппет с именем CacheAccelerator.
image

image

image

image

Туда копируем содержимое сниппета.

Сниппет CacheAccelerator:

<?php
//Функция сравнения. Работает в соответствии с функцией сравнения Ditto
if(!function_exists(cacheFieldsCompare)) {
function cacheFieldsCompare ($param1, $param2, $param3){
/*
1 or != Не равно
2 or = Равно
3 or < Меньше чем
4 or > Больше чем
5 or <= Меньше чем или равно
6 or >= Больше чем или равно
7 Содержит
8 Не содержит
*/
switch($param3){
case 1:
return $param1 != $param2;
break;
case 2:
return $param1 == $param2;
break;
case 3:
return $param1 < $param2;
break;
case 4:
return $param1 > $param2;
break;
case 5:
return $param1 <= $param2;
break;
case 6:
return $param1 >= $param2;
break;
case 7:
return stristr($param1, $param2);
break;
case 8:
return !stristr($param1, $param2);
}
}
}

$nocache = isset($nocache)? $nocache : 0; //флаг необходимости сброса кэша
$url = $_SERVER["REQUEST_URI"]; //текущий урл, включается в ключ для кэширования
$path_to_cacheengine=$modx->config['base_path']."assets/plugins/cacheaccelerator/"; //путь к директории с CacheAccelerator
$path_to_cache=$modx->config['base_path']."assets/plugins/cacheaccelerator/cache/"; //путь к директории для хранения кэша (может быть произвольым)
require_once ($path_to_cacheengine."fileCache.php"); //запрос класса fileCache
$cache = fileCache::GetInstance(84600*7,$path_to_cache);//создание инстанции класса fileCache

//обработка флага принудительной очистки кэша
if((int)$clearCache){
if($logMessages) echo("Clearing cache...");
$cache->deleteCache(0);
return;
}

//обработка групп пользователей, для которых кэширование не производится (администраторы сайта, модераторы и тд)
$noCacheGroups = isset($noCacheGroups) ? $noCacheGroups : "";
$nocache = intval($modx->isMemberOfWebGroup(explode("||",$nocacheGroups)) || $modx->checkSession()) ? 2 : $nocache;
if($nocache == 2){
if($logMessages) echo("No caching for this web group.");
}

/* обработка стоп-полей, дающих сигнал на сброс кэша. в случае совпадения условий, кэш сбрасывается */
if(isset($dropCacheField)){
$fieldsArray = explode("||", $dropCacheField);
foreach ($fieldsArray as $field){
$field1 = explode(";", $field);
if($field1[1] && $field[2]){
if(empty($field1[0])){
foreach ($_POST as $key => $postField){
if(cacheFieldsCompare($postField, $field1[1], $field1[2])){
$nocache = 1;
continue;
}
}
foreach ($_GET as $key => $getField){
if(cacheFieldsCompare($getField, $field1[1], $field1[2])){
$nocache = 1;
continue;
}
}
} else {
if(!empty($_POST[$field1[0]])){
if(cacheFieldsCompare($_POST[$field1[0]], $field1[1], $field1[2])){
$nocache = 1;
continue;
}
}
if(!empty($_GET[$field1[0]])){
if(cacheFieldsCompare($_GET[$field1[0]], $field1[1], $field1[2])){
$nocache = 1;
continue;
}

}
}
} else {
if(!empty($_POST[$field1[0]]) || !empty($_GET[$field1[0]]))
$nocache = 1;
}
}

//непосредственно сброс кэша при совпадении условий
if($nocache == 1){
if($logMessages) echo("Clearing cache...");
$cache->deleteCache(0);
}
}

//запрос результата работы сниппета из кэша
if($nocache == 0){
$cached = $cache->cache($cacheId.$url);
if(isset($cached)){
if($logMessages) echo("Cache hit!");
$modx->placeholders = $cached['placeholders']; //установка плейсхолдеров закэшированного сниппета
return $cached['content']; //возврат результата работы сниппета из кэша
}
}

$output = $modx->runSnippet($snippetToCache, $modx->event->params); //непосредственное выолнение сниппета с передачей всех параметров

//помещение в кэш результата работы сниппета
if($nocache == 0){
if($logMessages) echo("Storing to cache...");
$cache->cache($cacheId.$url,array('placeholders' => $modx->placeholders, 'content' => $output));
}
//возвращение результата работы сниппета в парсер MODx
return($output);
?>


Созраняем созданный сниппет. Затем переходим во вкладку Плагины.
Нажимаем Создать плагин.
image

image

Задаем ему имя CacheAcceleratorClear. Копируем в него содержимое плагина.

Плагин CacheAcceleratorClear:

$path_to_cacheengine=$modx->config['base_path']."assets/plugins/cacheaccelerator/"; //путь к директории с CacheAccelerator
$path_to_cache=$modx->config['base_path']."assets/plugins/cacheaccelerator/cache/"; //путь к директории для хранения кэша (может быть произвольым, но должно совпадать со значением сниппета CacheAccelerator)
require_once ($path_to_cacheengine."fileCache.php");//запрос класса fileCache
$cache = fileCache::GetInstance(84600*7,$path_to_cache);//создание инстанции класса fileCache
$cache->deleteCache(0);//очистка кэша
return;


Внимание! После того как скопировали содержимое плагина, переходим во вкладку Системные события, в которой отмечаем галочкой событие OnCacheUpdate в разделе Cache Service Events!
Затем нажимаем кнопку Сохранить.
image
image
Все, установка CacheAccelerator завершена.

Данный плагин выполняет одну функцию. Когда MODx выполняет очистку своего кэша, он также выполняет очистку кэша CacheAccelerator

Использование CacheAccelerator:

CacheAccelerator может использоваться абсолютно для любых сниппетов. Кэшировать любую выдачу.
Я же пока рассмотрю его применение на примерах вызовов Ditto и Jot.

Вызов Ditto будет выглядеть следующим образом:
[[CacheAccelerator?
&snippetToCache=`Ditto`
&cacheId=`News`
&parents=`1`
&display=`10`
&paginate=`1`
&paginateAlwaysShowLinks=`1`
]]


Вместо самого вызова Ditto, вызывается сниппет CacheAccelerator, а название кэшируемого сниппета (в данном случае Ditto) указывается в параметре snippetToCache.
Далее следует параметр cacheId. Он создан для разделения кэшируемого контента в случае присутствия нескольких кэшируемых сниппетов на странице. Например, левое меню, последние новости и сама лента новостей. Может содержать любое значение, к которому привяжется закэшированный контент.
Заметим, что если Ditto вызывался в двойных скобках [[]], то и данный вызов должен также содержаться в них.

Пример вызова Jot:
[!CacheAccelerator?
&snippetToCache=`Jot`
&cacheId=`Comments`
&dropCacheField=`JotForm||post;true;2||;publish;2||;unpublish;2||;delete;2||;edit;2`
&noCacheGroups=`Site Admins`
&customfields=`name,email`
&pagination=`10`
&badwords=`*****`
&canmoderate=`Site Admins`
&captcha=`1`
!]


Сам кэшируемый сниппет также указывается в параметре snippetToCache, а в cacheId идентификатор кэшируемого контента на странице.
Здесь присутствуют еще два параметра dropCacheField и noCacheGroups. На них мы остановимся подробнее.

Дело в том, что, в отличие от новостных блоков, добавление которых происходит из менеджера MODx и где, после добавления каждой из новостей, очищается кэш, в ленте комментариев Jot любой пользователь зашедший на сайт может добавить свой комментарий. При этом, чтобы этот комментарий был виден как ему, так и другим пользователям, необходимо очистить кэш CacheAccelerator. Данное действие может потребоваться не только для Jot, но и для многих других сниппетов, выдачу которых хотелось бы закэшировать, но также предусмотреть возможность обновления кэша после каких-либо действий пользователя.
Для этих целей и служит параметр dropCacheField.
Он содержит список условий, разделенных ||. При срабатывании любого из этих условий происходит очистка кэша CacheAccelerator.

Условием может быть имя поля.
При обнаружении которого в запросах GET или POST, кэш будет обновлен.

Может быть сравнением.
Через точку с запятой перечисляются:
поле;значение;метод_сравнения
Если само имя поля указано пустым, то сравнению подвергаются все существующие поля.

Вот пример:
&dropCacheField=`JotForm||post;true;2||;delete;2`

Здесь указано:
  • Если в запросе присутствует поле JotForm
  • Если есть поле post и оно равно true
  • Если любое из полей имеет значение delete

При любом совпадении, будет произведена очистка кэша.

При постинге нового сообщения в Jot, передается форма, где содержится поле JotForm. Таким образом, после постинга комментария, кэш будет очищен и информация останется актуальной. После первого же запроса на эту страницу, кэш будет снова создан и последующие обращения станут отдаваться из кэша.

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

Очистить кэш CacheAccelerator также можно из любого места вызвав:
Из чанка:
[!CacheAccelerator? &clearCache=`1`!]

Из сниппета:
$modx->runSnippet("CacheAccelerator", array("clearCache" => 1))

Любые плейсхолдеры, заданные кэшируемым сниппетом, также кэшируются и прописываются при вызове из кэша.

Пример:
[[CacheAccelerator?&snippetToCache=`Ditto`&cacheId=`News`]]
Показаны [+start+] - [+stop+] из [+total+] Новостей<br>
[+previous+] [+pages+] [+next+]<br>


Список параметров:
snippetToCache — имя сниппета для кэширования, например `Ditto`.
cacheId — идентификатор закэшированного сниппета на странице, например `News`
dropCacheField — список полей и условий для них, при выполнении которых произойдет сброс кэша, например `JotForm||post;true;2`
noCacheGroups — список групп, для которых обработка кэширования не будет производиться, например `admins||moderators`
clearCache — при значении 1, происходит принудительная очистка кэша.
logMessages — при значении 1, перед содержанием сниппета, будут отображаться системные сообщения о попадании в кэш и тд.

Список условий:
1 != Не равно
2 = Равно
3 < Меньше чем
4 > Больше чем
5 <= Меньше чем или равно
6 >= Больше чем или равно
7 Содержит
8 Не содержит

Скачать готовую версию CacheAccelerator Вы можете на этом сайте.

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

UPD.


Добавились новые параметры сниппета:
noCacheRoles — список ролей менеджеров, для которых обработка кеширования не будет производиться
например `Administrator||Editor`

checkURL — создавать отдельный кэш для разных URL (1|0). По умолчанию включено. Включать полезно для сниппетов с постраничной навигацией.

А также новый параметр для плагина:
only_manual — разрешение только ручного сброс кэша.

За доработку большое спасибо Andchir!

Скачать новую версию можно по прежнему отсюда или отсюда
Tags:
Hubs:
+1
Comments 4
Comments Comments 4

Articles