MyTetra Share
Делитесь знаниями!
Кэширование данных
Время создания: 05.02.2018 10:01
Текстовые метки: highload cache
Раздел: Highload

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

  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 (бесконечное хранение), это может засорить память.

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 минут.

 
MyTetra Share v.0.53
Яндекс индекс цитирования