PHP Profi

Кэширование запросов с помощью PHP и MySQLnd Перевод Серия 

Несмотря на то что у MySQL есть встроенное кэширование запросов, оно всё равно имеет некоторые проблемы:

Плюсы Минусы
Простота использования: просто включить в конфиге MySQL Упрощённый: он ничего не знает о ваших нуждах
Прозрачно: не требуются изменения в приложении. Легко аннулируется: любые изменения в таблице сбросят все связанные с ней данные, даже если это не нужно
(см. Упрощённый)
  Однопоточный: так как кэш однопоточный, он может в действительности вредить производительности

Мы можем решить эти проблемы, используя кэширование на уровне приложения, но как нам достичь простоты и прозрачности использования, не создавая себе новых проблем?

И вот тут на сцену выходит плагин mysqlnd_qc.

Установка

Плагин является наименее стабильным из тех, которые мы изучаем в этой серии, и требует, чтобы вы использовали альфа-версию пакета для новых версий PHP (по крайней мере для 5.5+):

pecl install mysqlnd_qc-alpha

Storage Backends

Плагин поддерживает несколько движков для хранения кэша, известные как 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 и ваше собственное пользовательское хранилище.

2016-04-18 оригинал

Последние посты

Комментарии

авторизуйтесь или зарегистрируйтесь, чтобы оставить комментарий