ГлавнаяРазноеТранзитное кэширование в высоконагруженных проектах WordPress

Транзитное кэширование в высоконагруженных проектах WordPress

Кэширование является неотъемлемой частью любого высоконагруженного проекта на WordPress. Ранее мы уже обсуждали основы кэширования и кэширование объектов в WordPress. В этой статье мы рассмотрим транзитное кэширование в WordPress и в частности библиотеку TLC Transients.

Транзитное кэширование в WordPress

В ядре WordPress есть встроенный механизм транзитного кэширования, который позволяет сохранить данные на определенный промежуток времени. Данный тип кэширования хорошо подходит для сохранения результата долгих операций, и самым простым примером является обращение на внешние API, например Facebook.

Предположим следующую функцию:

function get_facebook_followers_count() {
    $result = wp_remote_get( 'https://graph.facebook.com/wpmag.ru' );
    $result = json_decode( wp_remote_retrieve_body( $result ) );
    return $result->likes;
}

echo "Количество лайков: " . get_facebook_followers_count();

Данная функция обращается к API Facebook, запрашивает объект (точнее страницу) wpmag.ru и возвращает количество лайков этой страницы. Время выполнения функции зависит от многих факторов, включая место положения вашего сервера относительно серверам Facebook, состояние и скорость сети и другое. В среднем функция может занимать 1-3 сек.

Это значит, что при использовании данной функции на сайте, время загрузки каждой страницы вырастет на 1-3 секунды. Более того, в случае вызова данной функции более чем 600 раз за 600 секунд, Facebook начнет выдавать ошибку вместо результата.

Пример транзитного кэширования

Чтобы ускорить эту функцию, можно воспользоваться транзитным кэшированием WordPress, и сохранить результат на 1 час:

function get_facebook_followers_count() {
    // Выдача из транзитного кэша
    $cached = get_transient( 'fb_followers' );
    if ( $cached !== false )
        return $cached;

    $result = wp_remote_get( 'https://graph.facebook.com/wpmag.ru' );
    $result = json_decode( wp_remote_retrieve_body( $result ) );
    $likes = $result->likes;

    // Запись в транзитный кэш на 1 час
    set_transient( 'fb_followers', $likes, 1 * HOUR_IN_SECONDS );

    return $likes;
}

Таким образом, при вызове этой функции первый раз, после получения запроса от Facebook, WordPress запишет результат в базу данных и в дальнейшем на протяжении часа будет выдавать этот результат из базы данных, не делая повторных запросов на сервер Facebook. А спустя час, функция снова обратиться к Facebook за данными.

С помощью такого подхода только один вызов этой функции в час увеличит время запроса на 1-3 секунды, а последующие вызовы будут выдаваться мгновенно.

Но для высоконагруженных проектов, к сожалению, даже такой вариант не подходит.

Транзитное кэширование в высоконагруженных проектах

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

В нашем примере обновление транзитного кэша занимает 1-3 секунды. При нагрузке в 50 запросов в секунду мы можем получить до 50-150 запросов, которые попытаются обновить этот же транзитный кэш.

Иными словами это 50-150 HTTP запросов на Facebook, и до 150 запросов с задержкой в 1-3 секунды (в лучшем случае). Для проекта подобного масштаба это катастрофа.

Такие проблемы часто называют «состоянием гонки» или race conditions, и решить их помогают блокировки. Например, перед запросом на Facebook для получения новых данных, наша функция может установить флажок, который станет сигналом для остальных запросов, что обновление данных уже в процессе, и повторные запросы к Facebook выполнять не стоит.

function get_facebook_followers_count() {
    // Выдача из транзитного кэша
    $cached = get_transient( 'fb_followers' )
    if ( $cached !== false )
        return $cached;

    // Установить блокировку
    $lock = my_acquire_lock( 'fb_followers' );
    if ( ! $lock )
        return 0;

    $result = wp_remote_get( 'https://graph.facebook.com/wpmag.ru' );
    $result = json_decode( wp_remote_retrieve_body( $result ) );
    $likes = $result->likes;

    // Запись в транзитный кэш на 1 час
    set_transient( 'fb_followers', $likes, 1 * HOUR_IN_SECONDS );

    // Снять блокировку
    my_release_lock( 'fb_followers' );
    return $likes;
}

Функции my_acquire_lock() и my_release_lock() являются псевдо-функциями, они здесь лишь для демонстрации. Механизм для блокировок может быть разным в зависимости от проекта, например сервер Memcached или Redis, GET_LOCK() в MySQL, или даже дополнительный транзит в WordPress.

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

Этот метод является далеко не идеальным по двум основным причинам:

  • Некоторые посетители смогут увидеть неточное количество лайков на протяжении 1-3 секунд
  • Первый запрос все равно добавит 1-3 секунды ко времени загрузки страницы

Можно конечно пойти дальше, записать последнее количество лайков в опцию и возвращать ее вместо нуля, а обновление опции можно вовсе сделать фоновым с помощью планировщика задач в WordPress. Но зачем изобретать колесо, когда есть библиотека TLC Transients.

Библиотека TLC Transients

TLC Transients — это библиотека, написана Марком Джейквитом, одним из ведущих разработчиков ядра WordPress. Она обеспечивает функционал схожий с обычным транзитным кэшем в WordPress, но имеет некоторые приятные дополнения:

  • При истечении срока кэша значение не удаляется
  • Обновление кэша происходит исключительно с помощью фонового запроса
  • Удобный синтаксис для получения и обновления кэша

Рассмотрим пример использования библиотеки TLC Transients на основе нашей задачи получить количество лайков страницы Facebook:

// Получить количество лайков страницы в Facebook
function get_facebook_followers_count() {
    $result = wp_remote_get( 'https://graph.facebook.com/wpmag.ru' );
    $result = json_decode( wp_remote_retrieve_body( $result ) );
    return $result->likes;
}

// Вывести количество лайков с помощью TLC Transients
echo tlc_transient( 'fb_followers' )
    ->updates_with( 'get_facebook_followers_count' )
    ->expires_in( 1 * HOUR_IN_SECONDS )
    ->background_only()
    ->get();

Функция get_facebook_followers_count обращается к Facebook API за данными, но в отличие от нашего первого примера, данную функцию мы никогда не вызываем напрямую. Вместо этого мы используем объект tlc_transient указав нашу функцию для обновления, время жизни кэша и принудительный фоновый режим.

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

Учтите, что функция передаваемая методу updates_with() должна существовать в основном контексте WordPress, так как процедура обновления выполняется асинхронно отдельно от текущего запроса.

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

Подписаться на рассылку

Подписаться → Подпишитесь на бесплатную рассылку журнала WP Magazine и получайте новости, события, подборки тем и плагинов, уроки, советы и многое другое в мире WordPress!

  • Vlad

    Спасибо, очень полезно.

  • Было бы хорошо это делать все плагином

    • Если вы о TLC Transients то я бы не стал писать плагин с зависимостью от другого плагина, поскольку в WordPress пока нет хорошего механизма работы с зависимостями. Лучше использовать как библиотеку в своем плагине, проверив наличие предварительно с помощью class_exists().

  • Эльдар

    Добрый вечер, Константин.

    Прошу прощения за наверное ламерский вопрос )
    Есть ли смысл в использовании транзитного кэша при запросе WP_Query участвующего в такого вида функции :

    $meta_query[] = array(
    ‘key’ => ‘_property_url’,
    ‘value’ => $url
    );
    $query = new WP_Query( $search_args );
    $in =($query->have_posts());
    return $in;

    Функция запускается по крону(раз в час) и используется при парсинге внешнего сайта и проверяет есть ли объявление с таким $url в нашей базе данных, если есть парсинг прерывается проверяются следующие объявления, если нет обновление парсится в базу.

    Или лучше просто использовать кастомный запрос к БД с помощью класса $wpdb в этом случае ???

    • Я бы в этом случае посоветовал простой запрос напрямую через $wpdb, поскольку WP_Query у вас будет запрашивать сами записи вместе с мета-данными. В любом случае, все подобные запросы на мета-данные являются медленными, поскольку в колонке meta_value индексов нет. Можно конечно самостоятельно добавить индекс фиксированной ширины, но при большом объеме он может стать неэффективным.

      Можно доп. колонку создать, или таблицу-зеркало с измененными типами и триггерами для ее обновления. А еще лучше использовать Elasticsearch или Sphinx для поиска данных. Но это только при больших объемах.

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

  • Ivan Panfilov

    Fluent interface — буээээ. через опции как стандартно в wordpress — не модно?
    запилили бы в виде функции и положили в ядро.
    еще не нравится что в БД хранит и сразу нельзя было блокировку сделать как параметр опции — зачем лишний код писать?
    вообще реализовали бы просто фцнкцию cacheable() в которую можно былоб передавать функцию формирующую результат.

    cacheable(‘get_facebook_followers_count’, $options)
    которая сама вызовет когда надо обновть или получить кеш

    • постоянно писать if как то раздражает

      А что вам мешает самостоятельно обернуть один if в собственную функцию один раз и везде ее использовать? А то что вы показали примером кода, это примерно то, что реализует библиотека TLC Transients.

      • Ivan Panfilov

        да ничего не мешает xD.
        было бы клево еслибы в ядре была одна такая функция с разными опциями.

  • the_pled

    Спасибо, отличная библиотека, к тому же функцию с параметрами кэшировать позволяет — то что нужно!

  • the_pled

    Столкнулся с проблемой связки TLC Transients + Memcached для кэширования объектов: при включении режима `background_only()` по запросу кэша выдаётся `false`. Проблема обсуждалась здесь: https://github.com/markjaquith/WP-TLC-Transients/issues/29, вместо предложенного изменения времени жизни кэша, решил проблему установкой redis сервера и соответсвующего плагина объектного кэширования: https://raw.githubusercontent.com/ericmann/Redis-Object-Cache/master/object-cache.php .

  • Сергей

    Подскажите, как правильно подключать библиотеку TLC Transients? Куда надо скопировать файлы с гитхаба, чтобы заработал последний пример? Сорри за нубский вопрос, но не могу понять.