Кэширование данных

Кэширование — это один из способов оптимизации Web приложений. В любом приложении встречаются медленные операции (SQL запросы или запросы к внешним API), результаты которых можно сохранить на некоторое время. Это позволит выполнять меньше таких операций, а большинству пользователей показывать заранее сохраненные данные. Медленный SQL запрос

Наиболее популярная технология кеширования для Web приложений — Memcache.

Когда нужно кэшировать

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

  • Используйте классы или функции, для работы с данными. Не используйте повторяющихся SQL выборок в основном приложении.
  • Используйте обертки для работы с внешними API.

Что кэшировать?

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

  • Результаты запросов к внешним сервисам (RSS, SOAP, REST и т.п.).
  • Результаты медленных выборок из базы данных.
  • Сгенерированные html блоки либо целые страницы.
Кэширование SQL запроса

Кэширование выборок из баз данных

Запросы к базе данных — наиболее распространенный пример. На основе Мemcache реализуется очень просто:

<?
memcache_connect('localhost', 11211);

function get_online_users()
{
	if ( !$list = memcache_get('online_users') )
	{
		$sql = 'SELECT * FROM users WHERE last_visit > UNIX_TIMESTAMP() - 60*10';
		$q = mysql_query($sql);
		while ($row = mysql_fetch_assoc($q)) $list[] = $row;
		memcache_set('online_users', $list, 60*60);
	}

	return $list;
}

$list = get_online_users();
...

# Запрос на получение пользователей кэшируется на 1 час

Обновление данных

Если Вы кэшируете данные, которые могут обновляться, необходимо очищать кэш после каждого обновления:

<?
memcache_connect('localhost', 11211);

function get_user($id)
{
	if ( !$data = memcache_get('user' . $id) )
	{
		$sql = 'SELECT * FROM users WHERE id= ' . intval($id);
		$q = mysql_query($sql);
		$data = mysql_fetch_assoc($q);
		memcache_set('user' . $id, $data, 60*60);
	}

	return $data;
}

function save_user($id, $data)
{
	mysql_query('UPDATE users SET ... WHERE id = ' . intval($id));
	memcache_delete('user' . $id);
}

Кэширование списков

Допустим, Вы кэшируете данные каждого пользователя, как в примере, а также их списки (например, список online пользователей). При обновлении данных пользователя, Вы удаляете данные из кэша только для указанного пользователя. Но его данные могут также присутствовать в списке online пользователей, которые тоже лежат в кэше. Сбрасывать списки при каждом обновлении данных любого пользователя не эффективно. Поэтому обычно используют такой подход:

  1. Кэшируют списки, которые состоят только из ID пользователей.
  2. Для вывода списка отправляют отдельный запрос для получения данных каждого пользователя.

Реализация выглядит так:

<?
memcache_connect('localhost', 11211);

function get_online_users()
{
	if ( !$list = memcache_get('online_users') )
	{
		$sql = 'SELECT id FROM users WHERE last_visit > UNIX_TIMESTAMP() - 60*10';
		$q = mysql_query($sql);
		while ($row = mysql_fetch_assoc($q)) $list[] = $row['id'];
		memcache_set('online_users', $list, 60*60);
	}

	return $list;
}

$list = get_online_users();
foreach ( $list as $id )
{
	$user = get_user($id);
	...
}

# Получим список ID пользователей и для каждого из них получим актуальные данные

Для получения данных сразу нескольких объектов можно использовать Multiget.

Повторные запросы

Некоторые данные могут запрашиваться несколько раз в рамках одной страницы, например:

<html>
<body>
<h1><?=htmlspecialchars( get_user( $_SESSION['id'] )['name'] )?></h1>
...
Email: <?=get_user( $_SESSION['id'] )['email']?>
...
<a href="/<?=get_user( $_SESSION['id'] )['nick']?>">Моя страница</a>
...

Каждый вызов get_user() будет получать данные из кэша. Если Memcache стоит на отдельном сервере, это вызовет большой сетевой трафик и задержки. Дополнительное кэширование

Чтобы этого избежать, можно использовать дополнительный кэш внутри самого приложения:

<?
memcache_connect('localhost', 11211);

function get_user($id)
{
	global $app_cache;
	if ( $app_cache['user' . $id] ) return $app_cache['user' . $id];

	if ( !$data = memcache_get('user' . $id) )
	{
		$sql = 'SELECT * FROM users WHERE id= ' . intval($id);
		$q = mysql_query($sql);
		$data = mysql_fetch_assoc($q);
		memcache_set('user' . $id, $data, 60*60);
		$app_cache['user' . $id] = $data;
	}

	return $data;
}

function save_user($id, $data)
{
	global $app_cache;

	mysql_query('UPDATE users SET ... WHERE id = ' . intval($id));
	memcache_delete('user' . $id);
	
	unset($app_cache['user' . $id]);
}

В реальных приложениях, имеет смысл иметь обертку для Memcache с дополнительным кэшом:

<?
class mem_cache
{
	private $inner_cache = [];

	public static function get( $key )
	{
		if ( array_key_exists($key, $this->inner_cache) ) return $this->inner_cache[$key];

		$data = memcache_get( $this->resource, $key );
		$this->inner_cache[$key] = $data;

		return $data['value'];
	}

	public static function set( $key, $value, $ttl )
	{
		memcache_set($key, $value, $ttl);
		$this->inner_cache[$key] = $value;
	}

	public static function del( $key )
	{
		memcache_delete($key);
		unset($this->inner_cache[$key]);
	}
}

# $inner_cache хранит дополнительный кэш

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

<?
class mem_cache
{
	private $inner_cache = [];
	public static $inner_cache_enabled = true;

	public static function get( $key )
	{
		if ( self::$inner_cache_enabled && array_key_exists($key, $this->inner_cache) ) return $this->inner_cache[$key];

		$data = memcache_get( $this->resource, $key );
		$this->inner_cache[$key] = $data;

		return $data['value'];
	}

	public static function set( $key, $value, $ttl )
	{
		memcache_set($key, $value, $ttl);
		if ( self::$inner_cache_enabled ) $this->inner_cache[$key] = $value;
	}

	public static function del( $key )
	{
		memcache_delete($key);
		unset($this->inner_cache[$key]);
	}
}

...
mem_cache::$inner_cache_enabled = false;

# Отключаем внутренний кэш

Подогревание

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

<?
memcache_connect('localhost', 11211);

function get_rss($id)
{
	if ( !$data = memcache_get('rss') )
	{
		$data = file_get_contents('http://rss.com/rss');
		memcache_set('rss', $data, 60*60);
	}

	return $data;
}

function update_rss_feed($id, $data)
{
	# операции по обновлению внешних ресурсов
	
	$data = file_get_contents('http://rss.com/rss');
	memcache_set('rss', $data, 60*60);
}

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

Время жизни (ttl)

ttl (время жизни) — это время, после которого, данные будут удалены из кэша. В Memcache устанавливается в секундах:

<?
memcache_set('rss', $data, 60*60);

# Установка ttl на 1 час

Чаще всего ttl ставят от нескольких минут до нескольких дней. Не используйте значение 0 (бесконечное хранение), это может засорить память. ttl кэша

LRU

Любой кэш работает по принципу вытеснения если ему не хватает памяти. Т.е. если Memcache может использовать максимум 1G памяти, а Вы пытаетесь сохранить ключей на 2G, то половину из этих данных Memcache удалит. Для определения, какие именно ключи удалять, используется алгоритм LRU (Least Recently Used): LRU

Memcache постарается удалить прежде всего те данные, которые запрашивались очень давно (т.е. менее популярные удалит, а более популярные оставит).

Кэширование очень медленных запросов

Представьте, что у Вас есть запрос, который выполняется 10 секунд. Вы сохраняете его в кэш на 1 час. Когда проходит это время, данные в кэше удаляются. В первые 10 секунд после этого Вы сталкиваетесь с ситуацией, когда несколько пользователей одновременно вызывают этот тяжелейший запрос. Это может привести к катастрофическим последствиям, т.к. в течение 10 секунд может быть несколько сотен или тысяч таких вызовов.

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

Атомарные операции

Иногда в кэше хранятся счетчики (например, количество пользователей). При добавлении новых пользователей, вместо сброса счетчика и повторной выборки, можно просто увеличить значение кэша на единицу. Но сделать это через приложение нельзя, т.к. это приведет к потере данных от двух одновременно выполненных запросов:

<?
$count = memcache_get('count');
$count++;
memcache_set('count', $count);

Memcache поддерживает две атомарные операции увеличения и уменьшения чисел:

<?
memcache_increment('count');

# Увеличит счетчик на 1, функция memcache_decrement() уменьшает счетчик

Самое важное

Кэширование в приложениях на основе Memcache — это очень сильный инструмент. Не забывайте, что Memcache не гарантирует Вам сохранности данных. Это значит, что нельзя рассчитывать на то, что сохраненные на 60 минут данные будут находиться в кэше именно 60 минут.


Подпишитесь на Хайлоад с помощью Google аккаунта
или закройте эту хрень