Временные опции (Transients API)

В WordPress есть механизм временного кэширования — так называемое Транзитное кэширование. При таком кэшировании данные сохраняются в Базу данных WordPress, в таблицу wp_options на указанный промежуток времени.

Данный тип кэширования подходит для сохранения результата сложных или долгих операций. Например сохранение результата HTTP запроса, или сохранение результата тяжелого SQL запроса.

Временные опции (Транзитные опции, Transients) стандартизируют кэширование данных в базу данных (или объектный кэш). Данные сохраняются в виде пары ключ = значение для которых указывается время жизни. по истечении времени жизни данные будут удалены или перезаписаны (будет удалена строка в БД).

Временные опции идентичны обычным опциям WordPress. Отличаются лишь тем, что ко временным опциям добавляется время жизни опции.

ВАЖНО! Если у вас включено постоянное объектное кэширование, то вся логика Транзитного кэширования автоматически начинает работать с кэшем объектов, т.е. данные в этом случае сохраняются туда, а не в Базу данных.

Авто-загрузка опций

Как вы возможно знаете, все опции с флагом autoload в таблице wp_options запрашиваются одним запросом и сохраняются в объектный кэш. См. параметр $autoload в add_option().

Однако, если для временной опции установлено время жизни - параметр $expiration, то такая опция не будет загружаться автоматически в объектный кэш, а при её получении будет делаться два простых запроса к БД: один на получение самой опции, другой на получения времени жизни опции.

Все Функции транзитных опций

get_transient( $transient )
Получает значение временной опции.
set_transient( $transient, $value, $expiration )
Устанавливает или обновляет временную опцию.
get_site_transient( $transient )
Получает значение временной опции главного сайта сети.
set_site_transient( $transient, $value, $expiration )
Устанавливает или обновляет временные данные для сайта в сети мультисайт.
delete_expired_transients( $force_db )
Удаляет все просроченные временные опции (транзитные опции).
delete_site_transient( $transient )
Deletes a site transient.
delete_transient( $transient )
Удаляет временную опцию.

Использование транзитных опций

Самый простой пример

Допустим нам нужно получить HTML с удаленного сайта и вывести его на своем сайте. Пусть это будет блок последних новостей.

$key = 'last_news_fetch';

$html = get_transient( $key );

if ( $html === false ) {

	// получаем HTML
	$resp = wp_remote_get( 'https://example.com/api/last-news' );
	$html = wp_remote_retrieve_body( $resp );

	set_transient( $key, $html, HOUR_IN_SECONDS );
}

echo $html;

Готво, теперь запрос будетделаться тольк раз в час.

Тут важно еще очистить полученый HTML код для безопасности, но этот момент я пропустил в коде для простоты восприятия. Для очистки можно использовать wp_kses().

Получим лайки из API Facebook

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

function get_facebook_followers_count() {

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

	return $result->likes;
}

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

Время выполнения этой функции огромное и может занимать 0.5-2 секунды. Это значит, что при использовании данной функции на сайте, время загрузки каждой страницы вырастет на это время.

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

function get_facebook_followers_count() {

	$cache_key = 'fb_followers';

	// Выдача из транзитного кэша
	$cached = get_transient( $cache_key );

	if ( $cached !== false ) {
		return $cached;
	}

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

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

	return $likes;
}

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

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

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

Сериализация значений

Если значение нужно сериализовать, то делать это отдельно не обязательно, WP делает это автоматически перед сохранением.

Специальные константы времени

В WordPress есть специальные константы времени, созданные для удобства:

MINUTE_IN_SECONDS  // 60 (секунд)
HOUR_IN_SECONDS    // 60  * MINUTE_IN_SECONDS
DAY_IN_SECONDS     // 24  * HOUR_IN_SECONDS
WEEK_IN_SECONDS    // 7   * DAY_IN_SECONDS
YEAR_IN_SECONDS    // 365 * DAY_IN_SECONDS

Удаление транзитных опций

Очистка временных опций

WP удаляет все просроченные транзитные опции автоматически по истечению срока давности. Для этого кроном запускается функция delete_expired_transients().

Когда нужно вручную удалить транзитную опцию можно использовать функцию delete_transient( $transient ). Это полезно в тех случаях, когда определенное действие (сохранение сообщения, добавление категории и т.д.) требует также обновление транзитной опции.

Например, нам нужно удалить временную опцию с названием special_query_results:

delete_transient( 'special_query_results' );

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

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

Источник

--

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