Асинхронная индексация sales order grid в Magento
Введение
В процессе создания и редактирования заказа в Magento происходит копирование данных основной таблицы заказов в вспомогательную для отображения списка заказов в административной части сайта. При этом основные данные заказа хранятся в таблице sales_flat_order, а данные, используемые для вывода списка заказов в таблице sales_flat_order_grid. Таблица sales_flat_order_grid дублирует часть колонок таблицы sales_flat_order вместе с данными и служит для ускорения выборки данных.
Поскольку в процессе создания заказа сохранение, а следовательно, и синхронизация данных между указанными таблицами выполняется несколько раз, может наблюдаться сильное замедление процесса создания заказа. Сторонние модули могут значительно увеличивать количество сохранений заказа в процессе оформления. Также ситуация усугубляется при большом количество пользователей на сайте и одновременном размещении нескольких заказов.
При мониторинге сайта в системе New Relic, можно увидеть вышеозначенную проблему примерно в таком виде.
На выделенном фрагменте видно, что вставка в таблицу sales_flat_order_grid заняла больше 27 секунд.
Асинхронные операции в Magento
Частой практикой в Magento является перенос некоторых операций в фоновый режим для увеличения скорости работы сайта, то есть выполнение их асинхронно. Обычно для этих целей используют планировщик задач cron, поддержка которого есть в Magento по умолчанию. Например, в Magento Community Edition отправка писем о заказе выполняется в фоновом режиме, а в Magento Enterprise Edition есть режим обновления индексов по расписанию, когда при изменении сущностей происходит сохранения идентификатора сущности в специальную таблицу (так называемый change log), а непосредственно индексация выполняется уже в фоне.
Асинхронная индексация sales_flat_order_grid в Magento 1 отсутствует, однако её можно реализовать самостоятельно по тому же принципу, который использует Magento Enterprise Edition для выполнения индексации по расписанию.
Добавление асинхронной индексации для sales_flat_order_grid
Для начала создадим каркас модуля, который в дальнейшем будет реализовывать нашу логику.
Создадим следующую структуру папок и файлов:
app/code/community/Vendor/GridReindex/etc/config.xml
app/code/community/Vendor/GridReindex/Helper/Data.php
app/etc/modules/Vendor_GridReindex.xml
Содержимое файла app/code/community/Vendor/GridReindex/etc/config.xml:
<?xml version="1.0"?> <config> <modules> <Vendor_GridReindex> <version>0.1.0</version> </Vendor_GridReindex> </modules> <global> <helpers> <vendor_gridreindex> <class>Vendor_GridReindex_Helper</class> </vendor_gridreindex> </helpers> </global> </config> </code>
Содержимое файла app/code/community/Vendor/GridReindex/Helper/Data.php:
<?php class Vendor_GridReindex_Helper_Data extends Mage_Core_Helper_Abstract {}
Содержимое файла app/etc/modules/Vendor_GridReindex.xml:
<?xml version="1.0" encoding="UTF-8"?> <config> <modules> <Vendor_GridReindex> <active>true</active> <codePool>community</codePool> </Vendor_GridReindex> </modules> </config>
Название "Vendor" выбрано произвольно и может быть заменено на название вашей компании.
Проверим, что модуль создан правильно и включен. Для этого нужно зайти в System->Config->Advanced в административной части и убедиться, что модуль Vendor_GridReindex присутствует в списке.
Теперь нам нужно создать таблицу в базе данных, для хранения id заказов, которые требуют синхронизации с таблицей sales_flat_order_grid.
Добавим модель, resource-модель и setup resource в config.xml:
<?xml version="1.0"?> <config> <modules> <Vendor_GridReindex> <version>0.1.0</version> </Vendor_GridReindex> </modules> <global> <models> <vendor_gridreindex> <class>Vendor_GridReindex_Model</class> <resourceModel>vendor_gridreindex_resource</resourceModel> </vendor_gridreindex> <vendor_gridreindex_resource> <class>Vendor_GridReindex_Model_Resource</class> <entities> <order_grid_log> <table>vendor_grid_order_grid_log</table> </order_grid_log> </entities> </vendor_gridreindex_resource> </models> <helpers> <vendor_gridreindex> <class>Vendor_GridReindex_Helper</class> </vendor_gridreindex> </helpers> <resources> <vendor_gridreindex_setup> <setup> <module>Vendor_GridReindex</module> </setup> </vendor_gridreindex_setup> </resources> </global> </config>
Создадим файл install-скрипта app/code/community/Vendor/GridReindex/sql/vendor_gridreindex_setup/install-0.1.0.php:
<?php /* @var $installer Mage_Core_Model_Resource_Setup */ $installer = $this; $installer->startSetup(); /** * Create table 'vendor_gridreindex/order_grid_log' */ $table = $installer->getConnection() ->newTable($installer->getTable('vendor_gridreindex/order_grid_log')) ->addColumn('version_id', Varien_Db_Ddl_Table::TYPE_BIGINT, null, array( 'primary' => true, 'unsigned' => true, 'identity' => true, 'nullable' => false, ), 'Version Id') ->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array( 'unsigned' => true, 'nullable' => false, ), 'Order Id') ->setComment('Order Grid Log'); $installer->getConnection()->createTable($table); $installer->endSetup();
После очистки кеша и открытия сайта в базе данных должна быть создана таблица vendor_grid_order_grid_log.
Затем нужно найти место в коде, где происходит копирование данных из sales_flat_order в sales_flat_order_grid. Это происходит в файле app/code/core/Mage/Sales/Model/Abstract.php в функции afterCommitCallback.
Кроме того, синхронизация данных происходит при сохранении отдельного атрибута заказа функцией saveAttribute в файле app/code/core/Mage/Sales/Model/Resource/Order/Abstract.php
В обоих случаях происходит вызов функции updateGridRecords у resource-модели заказа (абстрактный класс Mage_Sales_Model_Resource_Order_Abstract), с передачей в функцию id измененных сущностей.
Рассмотрим эту функцию:
public function updateGridRecords($ids) { if ($this->_grid) { if (!is_array($ids)) { $ids = array($ids); } if ($this->_eventPrefix && $this->_eventObject) { $proxy = new Varien_Object(); $proxy->setIds($ids) ->setData($this->_eventObject, $this); Mage::dispatchEvent($this->_eventPrefix . '_update_grid_records', array('proxy' => $proxy)); $ids = $proxy->getIds(); } if (empty($ids)) { // If nothing to update return $this; } $columnsToSelect = array(); $table = $this->getGridTable(); $select = $this->getUpdateGridRecordsSelect($ids, $columnsToSelect); $this->_getWriteAdapter()->query($select->insertFromSelect($table, $columnsToSelect, true)); } return $this; }
Перед тем как выполнить синхронизацию происходит вызов события, в которое передается proxy-объект со списком ids. Мы можем воспользоваться этим событием для нашей цели. Значение переменной $this->_eventPrefix для заказа будет равно "sales_order_resource".
Добавляем обработчик события sales_order_resource_update_grid_records в секцию frontend файла config.xml:
<frontend> <events> <sales_order_resource_update_grid_records> <observers> <vendor_gridreindex_log_order_ids> <class>vendor_gridreindex/observer</class> <method>logOrderIds</method> </vendor_gridreindex_log_order_ids> </observers> </sales_order_resource_update_grid_records> </events> </frontend>
Содержимое файла app/code/community/Vendor/GridReindex/Model/Observer.php:
<?php class Vendor_GridReindex_Model_Observer { public function logOrderIds(Varien_Event_Observer $observer) { $proxy = $observer->getProxy(); $ids = $proxy->getIds(); if (!empty($ids)) { /** @var Mage_Core_Model_Resource $resource */ $resource = Mage::getSingleton('core/resource'); $connection = $resource->getConnection('write'); $connection->insertArray( $resource->getTableName('vendor_gridreindex/order_grid_log'), array('entity_id'), $ids ); $proxy->setIds(array()); } return $this; } }
Здесь мы сохраняем ids заказов в нашу таблицу, и обнуляем ids у proxy-объекта, для отмены стандартной синхронизации.
Теперь, при каждом сохранении заказа в таблицу vendor_grid_order_grid_log будет сохраняться id сущности, что означает необходимость обновления данных в sales_flat_order_grid. Мы добавили обработчик в секцию frontend для того чтобы в административном интерфейсе всё работало по-старому, и изменения отображались в таблице заказов незамедлительно.
Осталось выполнить синхронизацию данных по крону. Добавим новую cron-задачу в config.xml после секции global:
<crontab> <jobs> <vendor_gridreindex_update_sales_grid> <schedule> <cron_expr>always</cron_expr> </schedule> <run> <model>vendor_gridreindex/observer::updateSalesGridByCron</model> </run> </vendor_gridreindex_update_sales_grid> </jobs> </crontab>
Параметр always говорит о том, что функция updateSalesGridByCron будет запускаться при каждом вызове magento cron, обычно это одна минута.
Добавляем функцию updateSalesGridByCron в файл app/code/community/Vendor/GridReindex/Model/Observer.php:
<?php public function updateSalesGridByCron() { /** @var Mage_Core_Model_Resource $resource */ $resource = Mage::getSingleton('core/resource'); $connection = $resource->getConnection('write'); $logTable = $resource->getTableName('vendor_gridreindex/order_grid_log'); $entityIds = $connection->fetchPairs( $connection->select()->from($logTable, array('version_id','entity_id')) ); if (!count($entityIds)) { return $this; } ksort($entityIds); /** @var Mage_Sales_Model_Resource_Order $salesResource */ $salesResource = Mage::getModel('sales/order')->getResource(); $salesResource->updateGridRecords(array_unique(array_values($entityIds))); $connection->delete($logTable, $connection->quoteInto('version_id IN (?)', array_keys($entityIds))); return $this; }
Наш обсервер достает id всех изменившихся заказов из таблицы лога, обновляет таблицу sales_flat_order_grid для этих id и удаляет их из таблицы лога. Мы получаем дополнительный прирост скорости за счёт того, что одновременно обновляем сразу несколько заказов, а не каждый по отдельности.
Содержимое файла app/code/community/Vendor/GridReindex/etc/config.xml:
<?xml version="1.0"?> <config> <modules> <Vendor_GridReindex> <version>0.1.0</version> </Vendor_GridReindex> </modules> <global> <models> <vendor_gridreindex> <class>Vendor_GridReindex_Model</class> <resourceModel>vendor_gridreindex_resource</resourceModel> </vendor_gridreindex> <vendor_gridreindex_resource> <class>Vendor_GridReindex_Model_Resource</class> <entities> <order_grid_log> <table>vendor_grid_order_grid_log</table> </order_grid_log> </entities> </vendor_gridreindex_resource> </models> <helpers> <vendor_gridreindex> <class>Vendor_GridReindex_Helper</class> </vendor_gridreindex> </helpers> <resources> <vendor_gridreindex_setup> <setup> <module>Vendor_GridReindex</module> </setup> </vendor_gridreindex_setup> </resources> </global> <frontend> <events> <sales_order_resource_update_grid_records> <observers> <vendor_gridreindex_log_order_ids> <class>vendor_gridreindex/observer</class> <method>logOrderIds</method> </vendor_gridreindex_log_order_ids> </observers> </sales_order_resource_update_grid_records> </events> </frontend> <crontab> <jobs> <vendor_gridreindex_update_sales_grid> <schedule> <cron_expr>always</cron_expr> </schedule> <run> <model>vendor_gridreindex/observer::updateSalesGridByCron</model> </run> </vendor_gridreindex_update_sales_grid> </jobs> </crontab> </config>
Наш модуль готов! Не забудьте проверить, что cron корректно настроен на сервере, так как без этого заказы не будут видны в административном интерфейсе.
Асинхронная индексация гридов в Magento 2
В Magento 2 был добавлен функционал асинхронной индексации таблиц гридов, в том числе заказов. По умолчанию данный функционал отключен и его можно включить здесь Stores->Configuration->Advanced->Developer->Grid Settings->Asynchronous indexing
Заключение
В этой статье мы выполнили оптимизацию процесса создания заказа, за счёт переноса части логики в фоновой режим. Благодаря этому скорость оформления заказа увеличилась, нагрузка на базу данных снизилась. Данный приём может быть использован не только с заказами, но и во множестве других похожих случаях.
UPD 18.04.2017. Текст статьи обновлен. Вместо перезаписи модели заказа теперь используется событие sales_order_resource_update_grid_records