Квест → Как хакнуть форму
Прошли: 77
Несмотря на то что у MySQL есть встроенное кэширование запросов, оно всё равно имеет некоторые проблемы:
Плюсы | Минусы |
---|---|
Простота использования: просто включить в конфиге MySQL | Упрощённый: он ничего не знает о ваших нуждах |
Прозрачно: не требуются изменения в приложении. |
Легко аннулируется: любые изменения в таблице сбросят все связанные с ней данные, даже если это не нужно (см. Упрощённый) |
Однопоточный: так как кэш однопоточный, он может в действительности вредить производительности |
Мы можем решить эти проблемы, используя кэширование на уровне приложения, но как нам достичь простоты и прозрачности использования, не создавая себе новых проблем?
И вот тут на сцену выходит плагин mysqlnd_qc.
Плагин является наименее стабильным из тех, которые мы изучаем в этой серии, и требует, чтобы вы использовали альфа-версию пакета для новых версий PHP (по крайней мере для 5.5+):
pecl install mysqlnd_qc-alpha
Плагин поддерживает несколько движков для хранения кэша, известные как storage handlers. Он включает в себя два встроенных обработчика хранения, и три, которые вы должны скомпилировать:
default
: хранилище в памяти для каждого процесса (встроенный)user
: хранилище, заданное пользователем (встроенный)memcache
: memcached хранилищеsqlite
: использовать sqlite для храненияapc
: использовать APC для хранения — требует и mysqlnd_qc, и скомпилированный APC, но может не работать с apcu (для PHP 5.5+)Если вы устанавливаете плагин с помощью команды PECL, то он спросит у вас, нужно ли установить в том числе и APC, Memcache или SQLite (в этом порядке)? Просто наберите «yes», чтобы включить их в установку.
Из всех вариантов хранилищ, memcache — это то хранилище, которое я рекомендую использовать; или как будет рассказано далее, вы сможете подключить своё собственное, используя «user
» обработчик.
Чтобы задать хранилище, которые вы хотите использовать, вам нужно вызвать функцию mysqlnd_qc_set_storage_handler()
и передать в неё один из вариантов обработчика хранилища описанных выше. Например, чтобы использовать memcache (или MySQL innodb memcache interface), вызовите:
mysqlnd_qc_set_storage_handler('memcache');
Настройки коннекта к memcache вы можете задать через INI-опции mysqlnd_qc.memc_server
и mysqlnd_qc.memc_port
. Это можно сделать либо в INI-файле, либо используя ini_set()
:
mysqlnd_qc.memc_server = 'localhost' mysqlnd_qc.memc_port = '11211'
или
ini_set('mysqlnd_qc.memc_server', 'localhost'); ini_set('mysqlnd_qc.memc_port', '11211');
По умолчанию используется localhost и порт 11211. К сожалению, вы можете указать только один memcache сервер, а не их список. Если вы хотите использовать несколько серверов, то вам понадобится создать свой пользовательский (user
) обработчик хранилища.
Плагин mysqlnd_qc позволяет вам прозрачно кэшировать все SELECT
-запросы, которые не содержат динамических колонок (например: NOW()
или LAST_INSERT_ID()
). Это можно сделать, установив INI-опцию mysqlnd_qc.cache_by_default
в 1
.
При включенной этой опции поведение очень схоже с кэшированием запросов самим MySQL, — во всех тех SELECT-ах, что кэшируются. Однако, так как это выполняется PHP процессом, оно не имеет тех проблем с коннектом, которые имеет однопоточный кэш MySQL.
Из коробки протухание кэша использует простой механизм, основанный на времени. Каждая единица кэша (запрос) содержит то, что называется TTL (Time To Live — время жизни) и по умолчанию устанавливается в 30 секунд.
Вы можете поменять TTL двумя способами:
Первый — установив INI-опцию mysqlnd_qc.ttl
в требуемое вам значение. Чем большее значение вы можете установить без последствий для вашего приложения, тем большую выгоду вы сможете получить из этого плагина.
Второй — используя SQL-подсказки (hint). Эта подсказка представлена константой MYSQLND_QC_TTL_SWITCH
, и может быть использована следующим образом:
$sql = sprintf("/*%s%d*/SELECT * FROM table", MYSQLND_QC_TTL_SWITCH, 10);
Т.о. получится следующий запрос:
/*qc_tt=10*/SELECT * FROM table
который установит TTL в 10 секунд.
Если вы решили по умолчанию включить кэширование, вы можете отказаться от этого для конкретного запроса, используя ещё одну SQL-подсказку - MYSQLND_QC_DISABLE_SWITCH
:
$sql = sprintf("/*%s*/SELECT * FROM table", MYSQLND_QC_DISABLE_SWITCH);
И получим запрос:
/*qc=off*/SELECT * FROM table
который отключит кэширование запроса.
В дополнение к базовому использованию по кэшированию всего или ничего и использованию SQL-подсказок чтобы изменить это поведение, mysqlnd_qc
также предоставляет возможность автоматического кэширования запросов, сделанных в отношении определенной базы данных и таблицы.
Сделать это можно с помощью функции mysqlnd_qc_set_cache_condition()
. Эта функция принимает три аргумента. Первый на данный момент всегда MYSQLND_QC_CONDITION_META_SCHEMA_PATTERN
, второй — шаблон, по которому искать, а третий — необязательный аргумент TTL. Если вы не указываете TTL, то будет использовано значение из настройки mysqlnd_qc.ttl
(см. выше).
Шаблон (второй аргумент) использует тот же синтаксис, как в MySQL-ном LIKE
операторе: %
соответствует любым одному и более символам, а _
соответствует любому одному символу.
Например, мы автоматически кэшировать данные сессии на 5 минут:
mysqlnd_qc_set_cache_condition(MYSQLND_QC_CONDITION_META_SCHEMA_PATTERN, "myapp.session", 5*60);
Или кэшировать все данные пользователя на 15 секунд:
mysqlnd_qc_set_cache_condition(MYSQLND_QC_CONDITION_META_SCHEMA_PATTERN, "myapp.user_%", 15);
Плагин mysqlnd_qc становится действительно интересным, когда вы погрузитесь в возможность кэширования по паттерну. Эта возможность позволяет вам указать callback-функцию, которая будет проверять каждый запрос на предмет, нужно ли его кэшировать и (опционально) сколько по времени хранить кэш.
Эта callback-функция должна возвращать один из трёх вариантов:
false
: запрос не должен быть закэшированtrue
: запрос должен быть закэширован на TTL по умолчанию, который прописан в ini-настройке mysqlnd_qc.ttl
<целое число>
: запрос должен быть закэширован на указанное количество секунд
Следует отметить, что возвращая true
или целое число, не гарантирует, что плагин сможет закэшировать результат в силу других факторов, таких, как недетерминированные SQL-запросы.
Мы можем использовать такую функцию, чтобы копировать поведение описанное выше программно:
function is_cacheable($sql) { if (preg_match("/SELECT (.*?) FROM session (.*)/ism", $sql) === 1) { return 5*60; } if (preg_match("/SELECT (.*?) FROM user_(.*?) (.*)/ism", $sql) === 1) { return 15; } return false; }
Затем мы указываем её в качестве callback-а:
mysqlnd_qc_set_is_select('is_cacheable');
Однако это становится ещё более мощным инструментом, как только мы начинаем добавлять больше логики приложения. Например, мы можем сделать так, чтобы не кэшировать при редактировании, или не кэшировать для пользователей с правами администратора. Авторизованные пользователи могут получать короткий ТТЛ кэша, в то время как неавторизованные пользователи могут получать более длительный.
И, наконец, последняя возможность, предоставляемая этим плагином — возможность писать полностью настраиваемые обработчики кэша. Это позволяет вам создать свой собственный бэкенд для управления хранилищем кэша.
Например, если вы решите использовать библиотеку кэша из вашего фреймворка, часто это позволяет в дальнейшем легко переключать адаптеры, чтобы использовать разные бэкенды для хранения, например, Cassandra или Riak.
Чтобы реализовать пользовательский обработчик кэша, необходимо определить семь callback функций или методов. Пока нет интерфейса предоставленного PHP, это может выглядеть примерно так:
namespace MySQLnd; interface CacheHandlerInterface { // Get a unique hash for the query, this is the cache-key public function get_hash($host_info, $port, $user, $db, $query); // Retrieve the data from the cache public function find_query_in_cache($key); // Called each time the data is pulled from the cache public function return_to_cache($key); // Add the query to the cache public function add_query_to_cache_if_not_exists($key, $data, $ttl, $run_time, $store_time, $row_count); // Called after the query executes to help maintain stats public function update_query_run_time_stats($key, $run_time, $store_time); // Get the stats public function get_stats($key = null); // Clear the cache completely public function clear_cache(); }
Если бы мы делали это при помощи Zend\Cache из Zend Framework 2, то в конечном итоге сделали бы что-то вроде этого:
namespace MyApp\Db\MySQLnd; use MySQLnd\CacheHandlerInterface; class CacheHandler implements CacheHandlerInterface { protected $cache; public function __construct(\Zend\Cache\Storage\StorageInterface $cache) { $this->cache = $cache; } public function get_hash($host_info, $port, $user, $db, $query) { return md5(sprintf("%s%s%s%s%s", $host_info, $port, $user, $db, $query)); } public function find_query_in_cache($key) { if ($this->cache->hasItem($key)) { return $this->cache->getItem($key); } return null; } public function return_to_cache($key) { $this->cache->touch($key); } public function add_query_to_cache_if_not_exists($key, $data, $ttl, $run_time, $store_time, $row_count) { $data = array( "data" => $data, "row_count" => $row_count, "valid_until" => time() + $ttl, "hits" => 0, "run_time" => $run_time, "store_time" => $store_time, "cached_run_times" => array(), "cached_store_times" => array(), ); if ($this->cache->hasData($key)) { return null; } $this->cache->getOptions()->setTtl($ttl); $this->cache->setItem($key, $data); return true; } public function update_query_run_time_stats($key, $run_time, $store_time) { if ($this->cache->hasKey($key . '_stats')) { $stats = $this->cache->getKey($key); } else { $stats = [ 'hits' => 0, 'run_times' => [], 'store_times' => [] ]; } $stats['hits']++; $stats['run_times'][] = $run_time; $stats['store_times'][] = $store_time; $this->cache->setItem($key . '_stats', $stats); } public function get_stats($key = null) { if ($key !== null && $this->cache->hasKey($key . '_stats')) { return $this->cache->getItem($key . '_stats'); } return []; } public function clear_cache() { if ($this->cache instanceof \Zend\Cache\Storage\FlushableInterface) { $this->cache->flush(); return true; } return false; } }
Далее, мы можем использовать это как-то так:
use MyApp\Db\MySQLnd\CacheHandler; // Get the cache service from the service locator $cache = $serviceLocator->getServiceLocator()->get('cache'); // Create an instance of our CacheHandler, passing in the Cache instance $ch = new CacheHandler($cache); // Setup the user handler \mysqlnd_qc_set_user_handlers([$ch, "get_hash"], [$ch, "find_query_in_cache"], [$ch, "return_to_cache"], [$ch, "add_query_to_cache_if_not_exists"], [$ch, "update_query_run_time_stats"], [$ch, "get_stats"], [$ch, "clear_cache"]);
Как только вы зарегистрируете свой собственный обработчик, он будет использоваться для выполнения всех действий в автоматическом режиме кэширования.
Обработчик кэша позволяет решить ряд проблем с кэшированием запросов MySQL, а также предоставляет интерфейс для легкой и прозрачной реализации кэширования в вашем приложении. Он дает возможность автоматизировать кэширование запросов, и поддерживает множество бэкендов для хранения кэша, включая memcached и ваше собственное пользовательское хранилище.