Кэширование — это один из способов оптимизации Web приложений. В любом приложении встречаются медленные операции (SQL запросы или запросы к внешним API), результаты которых можно сохранить на некоторое время. Это позволит выполнять меньше таких операций, а большинству пользователей показывать заранее сохраненные данные.
Наиболее популярная технология кеширования для Web приложений — Memcache.
Когда нужно кэшировать
Старайтесь избегать кэширования, пока в этом не будет прямой необходимости. Это простая техника, но это снижает гибкость приложения. Не делайте лишнюю работу заранее, но закладывайте возможность использования кэширования в будущем:
- Используйте классы или функции, для работы с данными. Не используйте повторяющихся SQL выборок в основном приложении.
- Используйте обертки для работы с внешними API.
Что кэшировать?
Кэшировать нужно данные, которые медленно генерируются и часто запрашиваются. На практике это обычно:
- Результаты запросов к внешним сервисам (RSS, SOAP, REST и т.п.).
- Результаты медленных выборок из базы данных.
- Сгенерированные html блоки либо целые страницы.

Кэширование выборок из баз данных
Запросы к базе данных — наиболее распространенный пример. На основе М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 пользователей, которые тоже лежат в кэше. Сбрасывать списки при каждом обновлении данных любого пользователя не эффективно. Поэтому обычно используют такой подход:
- Кэшируют списки, которые состоят только из ID пользователей.
- Для вывода списка отправляют отдельный запрос для получения данных каждого пользователя.
Реализация выглядит так:
<?
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 (бесконечное хранение), это может засорить память.
LRU
Любой кэш работает по принципу вытеснения если ему не хватает памяти. Т.е. если Memcache может использовать максимум 1G памяти, а Вы пытаетесь сохранить ключей на 2G, то половину из этих данных Memcache удалит. Для определения, какие именно ключи удалять, используется алгоритм LRU (Least Recently Used):
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 минут.